Compare commits

...
Author SHA1 Message Date
Cédric VerstraetenandCopilot 6f1dcf8f00 Release Hub 0.167.3 with hardened Admin routing and probes
Publish opt-in hardened runtime support with named Admin Service routing, configurable health probes, legacy defaults and runtime regression coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-28 18:27:10 +00:00
Cédric Verstraeten 371408e5eb Merge pull request #207 from kerberos-io/security/image-hardening-rollout
Add opt-in hardened Hub worker runtime contexts
2026-10-27 14:43:33 +01:00
Copilot 0eff4bf2ee feat(hub): support opt-in hardened Admin and notification images
Retain default Admin port 80 while allowing 8080 and explicit container security settings. Add notification-only pod and container contexts without changing its provider or FFmpeg contract. Cover routing, tmpfs, TLS, identity, default preservation and per-component isolation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-26 22:35:20 +00:00
Copilot 18e456d490 feat(hub): add opt-in hardened worker runtime contexts
Preserve legacy image and deployment defaults while supporting verified per-worker container and pod security contexts alongside existing writable volumes. Document draft image contracts and workload-sized scratch storage; validate all/pipeline/ui renders and TLS compatibility.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-26 22:21:31 +00:00
Cédric Verstraeten 374d40aa41 Merge pull request #206 from kerberos-io/fix/hardened-frontend-demo-routing
Fix hardened frontend runtime settings for demo deployments
2026-10-26 21:37:24 +01:00
Copilot 073c12f8eb Fix hardened frontend runtime settings for demo deployments
Apply the shared port and security context to the demo frontend so both deployments support the shell-free v1.19.21 image. Release chart 0.167.2.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-26 20:33:49 +00:00
Cédric VerstraetenandCopilot 001c835068 Merge pull request #205 from kerberos-io/security/frontend-image-runtime
Add opt-in frontend port and security context support for hardened images.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-26 20:18:36 +01:00
Cédric VerstraetenandCopilot b520e2ecc1 Support hardened frontend ports and security context
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-26 18:20:57 +00:00
Cédric Verstraeten c42388b96f Merge pull request #204 from kerberos-io/feature/hub-passkey-configuration
Add Hub API passkey configuration and bump chart to 0.167.0
2026-10-26 14:47:35 +01:00
Cédric VerstraetenandCopilot 04d6990460 feat(hub): configure API passkeys and bump chart to 0.167.0
Add opt-in API-only WebAuthn configuration, value validation, deployment documentation and rendering checks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-26 13:33:22 +00:00
KilianandClaude Opus 5.5 5af6472826 feat(hub): grouped device alerts — scan interval and device_group email (0.166.0) (#201)
* feat(hub): configurable device scan interval for hub-monitor-device (0.166.0)

kerberoshub.monitordevice.scanIntervalSeconds (default 60) is passed as
DEVICE_SCAN_INTERVAL_SECONDS: the pause between device status scans,
which also sets how long grouped device notifications are held.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(hub): device_group email for grouped camera status notifications

- custom-layout/templates/device_group.html|.txt: the email sent instead of
  device when several cameras of an account change status in one scan and
  the account groups device notifications.
- email.templates.deviceGroup / deviceGroupTitle, passed to
  hub-monitor-device as DEVICE_GROUP_TEMPLATE / DEVICE_GROUP_TITLE, plus
  PUBLIC_URL for the link to the cameras page.
- README: full reference for the device_group email (placeholders, how
  sections are built and rendered, custom templates, webhook
  device_statuses) and the correct {{status}} values of the device email.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(hub): device_group webhook data keys and whole-group section titles

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(hub): device_sections on grouped device webhooks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(hub): name the device grouping toggle Group notifications

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(hub): drop device_statuses/device_sections webhook fields

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(hub): grouped device notifications skip webhooks and IFTTT; SMTP-only email

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:54:16 +02:00
KilianandClaude Opus 5.5 e0a273422e feat(hub): one sampling model — follow-only downstream, per-class hub-api ratios (#200)
* feat(hub): per-class trace sampling ratios for hub-api

Add opentelemetry.apiSamplingRatios (operator, default), rendered for hub-api
as TRACE_SAMPLER_RATIO_<CLASS> (trace library v1.2.5+): operator covers user
and API-client requests, default everything else hub-api starts (device
heartbeats, agent uploads and live segments, background work). Empty values
are not rendered, so defaults leave the manifests unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(hub): one sampling model, follow-only downstream

Downstream services (hub-workflows, pipe-*, workflow stages) only continue
traces started at the Vault or hub-api, so they now always render
OTEL_TRACES_SAMPLER=parentbased_always_off when OpenTelemetry is enabled
instead of a free-form opentelemetry.sampler, which a non-parent-based value
would have turned into independent re-sampling. hub-api is configured only
through opentelemetry.apiSamplingRatios (TRACE_SAMPLER_RATIO_<CLASS>);
opentelemetry.apiSampler is removed. Neither removed value is used by any
environment except production's sampler.type: parentbased_always_off, which
is now the fixed behaviour, so production renders unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(hub): rename hub-api's catch-all sampling class to other

trace v1.2.6 renames the catch-all class from default to other and
rejects TRACE_SAMPLER_RATIO_DEFAULT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:45:08 +02:00
Cédric Verstraeten 71e2db095e Merge pull request #202 from kerberos-io/feature/pipeline-sequence-log-level
feature/pipeline-sequence-log-level
2026-10-23 10:54:42 +02:00
Cédric Verstraeten 356f510ba9 Merge branch 'main' into feature/pipeline-sequence-log-level 2026-10-23 10:45:44 +02:00
Cédric Verstraeten 95ffe47edb Configure sequence logs and thumbnail keyframes
Expose Sequence LOG_LEVEL with an info default and document its configuration. Add thumbnail skipNonKeyframes, enabled by default for faster extraction. Bump the Hub chart to 0.159.1.
2026-10-23 10:44:59 +02:00
KilianandClaude Opus 5.5 abb22276ca feat(hub): split the OTel sampler for hub-api and trace workflow stages (#199)
opentelemetry.sampler now applies only to the services downstream of the Vault
(hub-workflows, pipe-*, workflow stages), which continue the Vault's trace and
should run parentbased_always_off; hub-api, which starts its own traces for
user requests, gets opentelemetry.apiSampler. Workflow stages (hub-stage.yaml)
had no OTEL_EXPORTER_OTLP_ENDPOINT, so their tracing ran in no-op mode; they
now get the endpoint and the downstream sampler when opentelemetry is enabled.

Defaults are empty, so the only rendered change is the stage endpoint.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 13:58:44 +02:00
KilianandClaude Opus 5.5 331df9d4af docs(hub): document email template variables (#198)
Explain how email templates are selected (SMTP files under
/mail/templates vs built-in vs Mailgun), how the base and tenant URLs
are built from kerberoshub.frontend, and which value each {{variable}}
gets in the alert, camera status, account and sharing emails.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 16:08:18 +02:00
KilianandClaude Opus 5.5 17dd600872 feat(hub): make analysis thumbnail regeneration opt-in (0.163.0) (#197)
Add kerberospipeline.analysis.thumbnailRegeneration.enabled (default
false), passed to pipe-analysis as THUMBNAIL_REGENERATION_ENABLED. When
enabled, analysis regenerates thumbnails from the first frame of a
classified moving object, adding one thumbnail job per classified
recording. Requires an analysis image that reads the flag.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:22:29 +02:00
KilianandClaude Opus 5.5 4880dc1197 feat(hub): allow defining the classification catalog inline in values (0.162.0) (#196)
Restore the optional classificationCatalog values key removed in 0.155.0.
A nonempty list ({key, label, icon?}, unique keys) replaces the bundled
defaults in the chart's classification-catalog ConfigMap, so Hub API and
the bundled catalogs/classifications.json share it. Omitted or null keeps
the defaults; [] is rejected. Combining it with classificationCatalogEnabled:
false, classificationCatalogExistingConfigMap or a classifications.json
replacement fails rendering.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:24:00 +02:00
KilianandClaude Opus 5.5 805dfe405b feat(hub): add frontend Google Places API selector (0.161.0) (#195)
Add kerberoshub.frontend.googlemaps.placesApi, rendered as
GOOGLEMAPS_PLACES_API on the hub-frontend and hub-frontend-demo
deployments. Defaults to "legacy" so existing keys keep working; "new"
switches address autocomplete to Places API (New) with the same apikey.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:14:23 +02:00
Kilian e861dbecc3 Merge pull request #194 from kerberos-io/feat/workflow-label-triggers
feat(hub): device system tags and system-tag global workflows (0.160.0)
2026-10-16 13:57:39 +02:00
Kilian BouteandClaude Opus 5.5 a35e701cb2 feat(hub): align the system tag catalog with the chart's catalog conventions
- Catalog entries use the Hub shared choice catalog format (key, label) plus
  an optional description, as catalogs/classifications.json does.
- values.schema.json now validates deviceTags (enabled boolean; entries with
  a stored-form key, a label, an optional description; no unknown keys), so
  mistakes fail like other values; templates keep the uniqueness and
  workflow cross-checks.
- The catalog stays inline in values (Helm checks workflow systemTags against
  it) but reaches hub-api like the classification catalog: the
  device-system-tags ConfigMap mounted at /etc/kerberos/device-tags, named by
  DEVICE_SYSTEM_TAGS_FILE, with a checksum annotation. The DEVICE_SYSTEM_TAGS
  JSON env var is gone (hub-api#672 reads the file).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 08:24:21 +00:00
Kilian BouteandClaude Opus 5.5 7cddafbe93 feat(hub): harden the system tag gate for workflow definitions
The engine reads JSON keys case-insensitively and ignores unknown ones, so
the previous exact-path checks could be bypassed (SystemTags, nested or
shared conditions, nested stage needs) and a typo (systemTag:, trigger:)
silently removed a workflow's device scope.

- While deviceTags.system.enabled is false, any mention of system tags in a
  definition (case-insensitive, any depth) fails rendering.
- Definitions may only use id, enabled, triggers and stages, and triggers
  only the models.WorkflowTrigger fields, spelled exactly.
- deviceTags.system.enabled must be a boolean; the string "false" enabled it.

Production's definitions still render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 08:07:41 +00:00
Kilian BouteandClaude Opus 5.5 5c6198eff2 feat(hub): rename device labels to typed device tags (system tags)
Root deviceLabelsEnabled/deviceLabels become deviceTags.system.enabled and
deviceTags.system.catalog, leaving room for more tag types. hub-api gets
DEVICE_SYSTEM_TAGS, and workflow triggers select devices with systemTags
(condition path device.systemTags). Matches uug-ai/models#284.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:47:47 +00:00
Kilian BouteandClaude Opus 5.5 f2ab8c8505 feat(hub): gate device labels with a root deviceLabelsEnabled flag
Replace kerberoshub.workflows.labelTriggers (and hub-workflows'
WORKFLOW_LABEL_TRIGGERS_ENABLED) with one deployment-wide switch next to
the catalog. While it is off, hub-api gets an empty catalog and rendering
fails on any workflow that selects devices by label (trigger labels or
device.labels conditions).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:12:52 +00:00
Kilian BouteandClaude Opus 5.5 7472d44683 feat(hub): add the device label catalog
Root deviceLabels is the deployment's device label catalog: the only
labels owners can assign to devices (value, name, description). It is
rendered as DEVICE_LABELS on hub-api; empty (the default) disables
device labels.

Labels are a device concept, so the catalog lives at the root next to
the classification catalog; workflows only refer to it. Rendering fails
when a catalog value is invalid or duplicated, or when a workflow
trigger selects a label that is not in the catalog, so a typo cannot
silently match no device. labelTriggers stays the separate trust switch
for global workflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:47:14 +00:00
Kilian BouteandClaude Opus 5.5 004377e69d feat(hub): allow label-scoped global workflows (0.160.0)
Add kerberoshub.workflows.labelTriggers (default false), rendered as
WORKFLOW_LABEL_TRIGGERS_ENABLED on hub-workflows. It lets global workflow
definitions select devices across organisations by owner-set device
label (trigger `labels`); the engine refuses such definitions until it
is enabled. Owners set labels, so enable it only where owners are
trusted administrators (self-hosted), never on multi-tenant SaaS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:16:11 +00:00
KilianandClaude Opus 5.5 664c587300 feat(hub): make the dominant color stage opt-in (#193)
* feat(hub): make the dominant color stage opt-in

Add kerberospipeline.dominantColor.enabled (default false). It gates the
pipe-dominantcolor deployment and is projected to pipe-analysis as
DOMINANTCOLOR_ENABLED, so the producer and consumer follow one value.

Requires a hub-pipeline-analysis image that reads DOMINANTCOLOR_ENABLED:
older images always publish to kcloud-dominantcolor-queue.fifo, which
would back up once the consumer is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(hub): bump analysis to v1.8.22 for DOMINANTCOLOR_ENABLED

v1.8.22 is the first hub-pipeline-analysis release that reads the flag
(uug-ai/hub-pipeline-analysis#114). Chart version moves to 0.159.0 since
0.158.0 was taken on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:52:23 +02:00
KilianandClaude Opus 5.5 c538f3da0b feat(hub): add frontend day overview feature flag (0.158.0) (#192)
Add kerberoshub.frontend.features.dayOverview.enabled, rendered as
FEATURE_DAY_OVERVIEW_ENABLED on the hub-frontend and hub-frontend-demo
deployments. Defaults to "true" so the Day overview page stays visible.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:35:36 +02:00
KilianandClaude Opus 5.5 40b17c5c37 feat(hub): make sprite and thumbnail prefetch count configurable (#191)
Add kerberospipeline.{sprite,thumbnail}.prefetchCount, rendered as
RABBITMQ_PREFETCH_COUNT only when greater than 0, and bump the images to
the releases that read it (sprite v1.1.21, thumbnail v1.3.15).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:17:45 +02:00
KilianandClaude Opus 5.5 d7551babe1 feat(hub): expose OpenTelemetry trace sampler settings (#190)
Add opentelemetry.sampler.type / opentelemetry.sampler.arg, rendered as
OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG on every workload that already
receives OTEL_EXPORTER_OTLP_ENDPOINT (hub-api, hub-workflows and the pipe-*
services) via a shared hub.opentelemetry.samplerEnv helper. Both are empty by
default, so rendered manifests are unchanged and services keep the SDK
default of sampling every trace.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:23:25 +02:00
Cédric Verstraeten 7b43295a18 Merge pull request #189 from kerberos-io/fix/hub-api-cors-origins
fix/hub-api-cors-origins
2026-10-10 13:24:20 +02:00
Cédric VerstraetenandCopilot 2de3a6f7c3 Allow additional Hub API CORS origins
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-10 11:14:38 +00:00
Cédric Verstraeten 8441ed7825 Add DB cache options to hub analysis pipeline
Adds `kerberospipeline.analysis.dbCache.enabled` (default false) and `dbCache.ttl` (default "5m") values. These are passed to the analysis deployment as DB_CACHE_ENABLED and DB_CACHE_TTL env vars to cache hot-path MongoDB lookups (site vault per device) in memory. Site changes apply after at most one TTL, and each replica caches separately. Documented in the chart README and values.yaml.
2026-10-10 13:03:30 +02:00
ede18bb45f Package workflow contracts and document the contract format (0.155.0) (#187)
* Allow per-file replacement of bundled workflow configuration (0.155.0)

Bundle Start, ANPR and classification defaults as templates, add replace: true
items for overriding single bundled files, and retire inline classificationCatalog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Document contract presentation, params and Start trigger modes

Extend the contract schema with presentation, params and triggerModes, add
ANPR presentation and a params example, and describe contracts as the editor
allow-list with editorStages as fallback metadata.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Declare Start context fields for strict conditions

Bundle deviceName and objectClass condition-only Start fields and document that
conditions may only test declared contract fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Document the always-present Start node

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Document connection panels and schema-based field paths

Add panel sections, widgets and the Extra conditions picker to the contract
schema, a panel to the pose example, and explain that Start paths are validated
against the models condition root schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Align the bundled ANPR contract with the ANPR stage result

Describe results.anpr fields as models.WorkflowAnprResult defines them: drop
exists where it always matches, offer class and engine choices, remove the
confidence maximum, add seconds visible and a Plate/Recognition panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bundle a forwarder workflow contract

Add a presentation-only forwarder contract to the bundled workflow
configuration: hub-workflows-forwarder reads no per-node settings and
contributes no output fields. It can be replaced like the other bundled files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Declare ANPR node settings and extend contract params

Contract params may be multiselect, take options from a catalog file
(choices.file) and bound numbers with minimum/maximum. The bundled ANPR
contract declares hub-anpr's six node settings with the worker's defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Declare isolated custom forwarder destination settings

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-08 16:51:34 +02:00
KilianandClaude Opus 5.5 eafeb5f370 feat(hub): configure workflow run retention (#185)
Expose WORKFLOW_RUN_RETENTION_DAYS for hub-cleanup with a 0 (disabled) default
and document the setting.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:46:09 +02:00
KilianandCopilot d18e66318f fix(hub): restore English translations (#186)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-07 17:18:14 +02:00
KilianandCopilot 22a3864b1e Unify workflow ConfigMap configuration for Hub 0.154.0 (#184)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-06 11:39:21 +02:00
KilianandCopilot 38806cae0e Add additive workflow ConfigMaps for Hub 0.153.0 (#181)
* Add additive workflow ConfigMaps and bump Hub chart to 0.150.0

Preserve bundled Start metadata and shared classifications when adding custom contracts/catalogs. Reject conflicting mount paths and retain the 0.149.0 replacement option for compatibility.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Bump Hub chart to 0.151.0

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove workflow-specific CI checks and scripts

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Bump Hub chart to 0.153.0

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Support externally managed classification catalogs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-06 09:19:01 +02:00
57 changed files with 3615 additions and 649 deletions
@@ -1,36 +0,0 @@
name: Workflows queue and identity consistency
# Fails the build if the analysis producer, the workflows engine and the stage
# workers would render onto different WORKFLOWS_QUEUE names — the silent
# producer/consumer queue-name drift that leaves runs piling up with no
# consumer. Also validates stable workflow identities and API/engine definition
# consistency. Pure `helm template` render checks, no cluster required.
on:
workflow_dispatch:
pull_request:
paths:
- 'charts/hub/**'
- 'scripts/check-workflows-queue-consistency.sh'
- 'scripts/check-workflow-configuration.sh'
- '.github/workflows/workflows-queue-consistency.yaml'
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Helm
uses: azure/setup-helm@v4
with:
version: v3.16.2
- name: Check workflow queue and identity consistency
run: ./scripts/check-workflows-queue-consistency.sh charts/hub
- name: Check workflow configuration and classification catalog
run: bash scripts/check-workflow-configuration.sh charts/hub
+1 -1
View File
@@ -16,7 +16,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.152.0
version: 0.167.3
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
+896 -49
View File
File diff suppressed because it is too large Load Diff
@@ -1,11 +0,0 @@
[
{"key": "animal", "label": "Animal", "icon": "animal"},
{"key": "pedestrian", "label": "Pedestrian", "icon": "pedestrian"},
{"key": "cyclist", "label": "Cyclist", "icon": "cyclist"},
{"key": "motorbike", "label": "Motorbike", "icon": "motorbike"},
{"key": "lorry", "label": "Lorry", "icon": "lorry"},
{"key": "car", "label": "Car", "icon": "car"},
{"key": "handbag", "label": "Handbag", "icon": "handbag"},
{"key": "suitcase", "label": "Suitcase", "icon": "suitcase"},
{"key": "cell phone", "label": "Cell phone", "icon": "cell phone"}
]
@@ -1,44 +0,0 @@
# yaml-language-server: $schema=../../schemas/workflow-contract.schema.json
# Metadata only; no current API loader consumes this contract.
schemaVersion: 1
stage: start
contractVersion: 1
# Paths refer to the safe initial context, not the full event or credentials.
fields:
- id: device
label: Device
path: device.deviceKey
type: string
operators: [eq, in]
choices:
source: project.devices
- id: sites
label: Sites
path: device.siteIds.*
type: string
operators: [in]
choices:
source: project.sites
- id: groups
label: Groups
path: device.groupIds.*
type: string
operators: [in]
choices:
source: project.groups
- id: classifications
label: Initial classifications
path: inputs.classify.details.*.classified
type: string
operators: [in]
optional: true
choices:
file: ../catalogs/classifications.json
# Existing structured controls, not fields produced by Start.
controls:
- id: weeklySchedule
label: Weekly schedule
- id: conditions
label: Advanced conditions
+467 -24
View File
@@ -44,7 +44,8 @@
"incomplete": "Enter all 4 characters to continue.",
"unlock": "Unlock encrypted data",
"switchAccount": "or switch account",
"ariaCharacter": "Passphrase character"
"ariaCharacter": "Passphrase character",
"rememberCode": "Remember code for 24 hours on this device"
},
"darkMode": {
"switchToLight": "Switch to light mode",
@@ -124,6 +125,7 @@
"recording": "Recording",
"dayOverview": "Day overview",
"analytics": "Analytics",
"markers": "Markers",
"watchlist": "Watchlist",
"cases": "Cases",
"alerts": "Alerts",
@@ -134,7 +136,257 @@
"allOf": "All {{name}}s",
"accounts": "Accounts",
"roles": "Roles",
"audit": "Audit events",
"settings": "Settings",
"context": {
"label": "Workspace",
"title": "Switch workspace",
"open": "Choose organisation and project",
"resize": "Resize organisation column"
},
"projects": {
"label": "Project",
"open": "Show projects",
"search": "Search projects",
"noResults": "No matching projects",
"fallback": "Project 1",
"empty": "No projects",
"unavailable": "Projects unavailable",
"loading": "Loading projects..."
},
"organisations": {
"label": "Organisation",
"search": "Search organisations",
"noResults": "No matching organisations",
"current": "Current organisation",
"none": "No organisation",
"select": "Select organisation",
"unavailable": "Organisation unavailable",
"loading": "Loading organisations...",
"empty": "No organisations yet",
"create": "Create organisation",
"name": "Organisation name",
"modal": {
"title": "Create organisation",
"subtitle": "Add the essentials now. Company and contact details can be completed later.",
"create": "Create organisation",
"tabs": {
"general": "General",
"company": "Company",
"contact": "Contact & location"
},
"fields": {
"name": "Organisation name *",
"description": "Description",
"domain": "Email domain",
"legalName": "Legal name",
"tradingName": "Trading name",
"industry": "Industry",
"website": "Website",
"email": "Contact email",
"phone": "Contact phone",
"street": "Street address",
"street2": "Address line 2",
"city": "City",
"region": "State / region",
"postalCode": "Postal code",
"country": "Country"
},
"placeholders": {
"name": "For example, Acme Security",
"description": "What this organisation is used for",
"domain": "example.com",
"website": "https://example.com",
"email": "contact@example.com"
},
"errors": {
"form": "Please complete all the missing fields.",
"nameRequired": "Enter an organisation name.",
"nameLength": "Use at least 2 characters.",
"email": "Enter a valid email address."
}
},
"settings": {
"title": "Organisation settings",
"open": "Open organisation settings",
"loading": "Loading organisation...",
"loadFailed": "The organisation could not be loaded.",
"save": "Save changes",
"saving": "Saving...",
"tabs": {
"general": "General",
"company": "Company",
"address": "Billing address",
"contacts": "Contacts",
"regional": "Regional",
"users": "Users",
"subscription": "Subscription"
},
"cards": {
"userTitle": "{{count}} user",
"usersTitle": "{{count}} users",
"usersSubtitle": "People with access to this organisation",
"usersFooter": "Organisation users",
"viewUsers": "View users"
},
"subscription": {
"currentPlan": "Current plan",
"noPlan": "No active plan",
"description": "Manage the plan, billing method, limits, and integrations for this organisation.",
"manage": "Manage subscription",
"status": {
"no-plan": "No subscription is currently active.",
"active": "The subscription is active.",
"cancelled": "The subscription has been cancelled.",
"reached-limit": "The subscription limit has been reached."
}
},
"users": {
"title": "Organisation users",
"descriptionSingle": "{{count}} person currently has access to this organisation.",
"description": "{{count}} people currently have access to this organisation.",
"manage": "Manage users",
"inactive": "Inactive",
"empty": "No users found",
"emptyDescription": "Add users to give them access to this organisation."
},
"contacts": {
"primary": "Primary contact",
"financial": "Financial contact",
"technical": "Technical contact"
},
"fields": {
"name": "Organisation name *",
"description": "Description",
"domain": "Email domain",
"legalName": "Legal name",
"tradingName": "Trading name",
"registrationNumber": "Registration number",
"vatNumber": "VAT number",
"industry": "Industry",
"website": "Website",
"email": "Contact email",
"phone": "Contact phone",
"street": "Street address",
"street2": "Address line 2",
"city": "City",
"region": "State / region",
"postalCode": "Postal code",
"country": "Country",
"timezone": "Timezone",
"locale": "Locale",
"currency": "Currency",
"contactName": "Name",
"contactEmail": "Email",
"contactPhone": "Phone"
},
"placeholders": {
"domain": "example.com",
"website": "https://example.com",
"email": "contact@example.com",
"timezone": "Europe/Brussels",
"locale": "en-US",
"currency": "EUR"
},
"errors": {
"form": "Please correct the highlighted fields.",
"nameRequired": "Enter an organisation name.",
"nameLength": "Use at least 2 characters.",
"email": "Enter a valid email address."
}
}
},
"projects": {
"label": "Project",
"current": "Current project",
"none": "No project",
"select": "Select project",
"unavailable": "Project unavailable",
"loading": "Loading projects...",
"empty": "No projects yet",
"search": "Search projects",
"create": "Create project",
"name": "Project name",
"settings": {
"title": "Project settings",
"details": "Project details",
"tabs": {
"general": "General",
"credentials": "Credentials"
},
"encryption": "Encryption keys",
"publicKey": "RSA public key",
"privateKey": "RSA private key",
"legacyPublicKey": "Agent public key",
"legacyPrivateKey": "Agent private key",
"encryptionKey": "Encryption key",
"secretKeys": "Secret keys",
"generate": "Generate credentials",
"regenerate": "Regenerate credentials",
"generating": "Generating keys...",
"generateHint": "Generate keys for this project.",
"oneTimeHint": "Copy these keys now. They will not be shown again.",
"copyPublicKey": "Copy RSA public key",
"copyPrivateKey": "Copy RSA private key",
"copyLegacyPublicKey": "Copy Agent public key",
"copyLegacyPrivateKey": "Copy Agent private key",
"copyEncryptionKey": "Copy encryption key",
"pincode": {
"generateTitle": "Generate project credentials?",
"generateMessage": "Generate new keys for this project.",
"label": "PIN code (optional)",
"hint": "Enter exactly 4 digits to encrypt the stored encryption key. The PIN is never saved.",
"error": "Enter exactly 4 digits or leave the PIN empty.",
"statusLabel": "PIN protection",
"enabled": "Enabled",
"disabled": "Disabled"
},
"import": {
"open": "Bring your own keys",
"title": "Bring your own keys",
"description": "Import an RSA-4096 public key generated outside the platform and provide your encryption key.",
"replacementWarning": "This replaces the current credentials. Cameras using them may disconnect.",
"formatHint": "The public key may be OpenSSL PEM or single-line Base64 DER. The encryption key must contain exactly 32 hexadecimal characters.",
"encryptionKeyError": "Enter exactly 32 hexadecimal characters.",
"cancel": "Cancel",
"confirm": "Import credentials",
"importing": "Importing..."
},
"regenerateWarning": {
"title": "Regenerate project credentials?",
"message": "This immediately replaces the current public, private and encryption keys. This action cannot be undone.",
"cameraImpact": "Cameras using the current credentials may disconnect and must be configured with the new credentials.",
"cancel": "Cancel",
"confirm": "Regenerate credentials"
}
},
"modal": {
"title": "Create project",
"subtitle": "A project keeps its devices, sites and recordings separate from the rest of the organisation.",
"create": "Create project",
"fields": {
"name": "Project name *",
"slug": "Handle *",
"description": "Description"
},
"placeholders": {
"name": "For example, Warehouse North",
"slug": "warehouse-north",
"description": "What this project is used for"
},
"hints": {
"slug": "Lowercase letters, numbers and single hyphens. Used to reference the project later."
},
"errors": {
"form": "Please complete all the missing fields.",
"nameRequired": "Enter a project name.",
"nameLength": "Use at least 2 characters.",
"slugRequired": "Enter a handle.",
"slugFormat": "Use lowercase letters, numbers and single hyphens only.",
"slugLength": "Use at most 63 characters."
}
}
},
"plansIntegrations": "Plans & integrations",
"dataUploaded": "Data uploaded",
"dailyLimit": "{{percent}}% – {{usage}}/{{max}} GB daily limit",
@@ -208,6 +460,57 @@
"Vault": "Vault"
},
"pages": {
"audit": {
"title": "Audit events",
"refresh": "Refresh",
"loadError": "Audit events could not be loaded. Try again.",
"metadata": "Metadata",
"noMetadata": "No metadata was recorded for this event.",
"changes": "Changes",
"filters": {
"actor": "Actor",
"actorPlaceholder": "Name or ID",
"action": "Action",
"target": "Target",
"targetPlaceholder": "Name or ID",
"operation": "Operation",
"permission": "Permission",
"category": "Category",
"outcome": "Outcome",
"search": "Search all fields",
"searchPlaceholder": "Actor, action, target, device...",
"from": "From",
"to": "To",
"any": "Any",
"clear": "Clear filters"
},
"table": {
"timestamp": "Timestamp",
"actor": "Actor",
"action": "Action",
"target": "Target",
"outcome": "Outcome",
"details": "Event details",
"showDetails": "Show metadata",
"hideDetails": "Hide metadata"
},
"authorization": "Authorization",
"request": "Request",
"policy": "Policy",
"granted": "Granted",
"denied": "Denied",
"noResults": {
"title": "No audit events found",
"description": "Events will appear here as activity is recorded."
},
"pagination": {
"showing": "Showing {{start}}–{{end}} of {{total}}",
"rows": "Rows",
"previous": "Previous",
"next": "Next",
"page": "Page {{page}}"
}
},
"floorplans": {
"noneUploaded": "No floor plans uploaded yet",
"uploadFirstHint": "Upload your first floor plan to visualize your devices on this site",
@@ -260,6 +563,13 @@
"completeAllFields": "Please complete all the missing fields",
"nameExists": "This name already exists",
"badgeColor": "Badge color (HEX)",
"fineTuneLocation": "Fine-tune map position",
"fineTuneInstructions": "Move the map so the pin marks the exact site location.",
"latitude": "Latitude",
"longitude": "Longitude",
"invalidCoordinates": "Enter valid coordinates.",
"selectAddressBeforeFineTune": "Select an address before fine-tuning its map position.",
"resetLocation": "Reset position",
"vaultSaveToUse": "Save changes to use this Vault",
"vaultCheckCredentials": "Check credentials and try again"
},
@@ -304,6 +614,85 @@
"analytics": {
"manageAlerts": "Manage alerts",
"subscribePrompt": "Subscribe to a plan to generate notifications",
"markers": {
"title": "Markers",
"overview": "Overview",
"searchPlaceholder": "Search marker names, e.g. a license plate",
"clearFilters": "Clear filters",
"loading": "Loading markers",
"loadError": "Markers could not be loaded. Please try again.",
"invalidDateRange": "The start date must be before the end date.",
"openDetails": "Open marker details",
"openMarkerDetails": "Open details for marker {{name}}",
"openMedia": "Open recording",
"noMedia": "No linked recording",
"mediaErrorTitle": "Recording unavailable",
"mediaErrorMessage": "The recording linked to this marker could not be opened.",
"detail": {
"title": "Marker details",
"recording": "Marker recording",
"loadingRecording": "Loading linked recording...",
"videoUnavailable": "The linked recording has no playable video.",
"markerPosition": "Marker starts {{offset}} into this recording",
"openFullRecording": "Open full recording",
"overview": "Overview",
"start": "Start",
"end": "End",
"description": "Description",
"classification": "Classification",
"metadata": "Metadata",
"source": "Source",
"engine": "Engine",
"modelVersion": "Model version",
"boundingBox": "Bounding box",
"rawMetadata": "Raw metadata",
"comments": "Comments",
"identifiers": "Identifiers",
"markerId": "Marker ID",
"deviceId": "Device ID",
"siteId": "Site ID",
"groupId": "Group ID",
"organisationId": "Organisation ID",
"mediaKey": "Media key",
"audit": "Audit information"
},
"filters": {
"categories": "Categories",
"from": "From",
"to": "To",
"sort": "Sort markers"
},
"sort": {
"latest": "Latest first",
"oldest": "Oldest first",
"nameAsc": "Name A-Z",
"nameDesc": "Name Z-A"
},
"table": {
"timestamp": "Timestamp",
"marker": "Marker",
"categories": "Categories",
"device": "Device",
"duration": "Duration",
"source": "Source",
"signals": "Events & tags",
"events": "Events",
"tags": "Tags",
"confidence": "Confidence",
"media": "Recording"
},
"noResults": {
"title": "No markers found",
"description": "Adjust the filters or search for another marker name."
},
"pagination": {
"showing": "Showing {{start}}-{{end}}",
"rows": "Rows",
"previous": "Previous",
"next": "Next",
"page": "Page {{page}}"
}
},
"noAlerts": {
"description": "Create an alert or enable existing ones to start receiving analytics.",
"button": "Alerts"
@@ -340,6 +729,19 @@
"placeholderDescription": "To create your first case go to the media section, and select a media item. Create a new case and assign it to the relevant person.",
"goToMedia": "Go to Media",
"myCases": "My cases",
"loadError": "Something went wrong while loading the case. Please try again.",
"autoplayNextOn": "Autoplay next: On",
"autoplayNextOff": "Autoplay next: Off",
"mediaProcessing": "Processing",
"mediaFailed": "Failed",
"a11y": {
"toggleCase": "Toggle case details",
"statusOpen": "Case status: open",
"statusApproved": "Case status: approved",
"statusRejected": "Case status: rejected",
"loadingCases": "Loading cases",
"caseThumbnail": "Case thumbnail"
},
"actions": {
"reopenCase": "Reopen case",
"approveCase": "Approve case",
@@ -563,6 +965,8 @@
"filter": {
"date": "Date",
"region": "Region",
"regionExcluded": "Not in results",
"regionExcludedHint": "No region drawn on this camera, so it is left out of the results. Draw a region to include it again.",
"categories": "Categories",
"markers": "Markers",
"events": "Events",
@@ -570,7 +974,15 @@
"newest": "Newest",
"oldest": "Oldest",
"showAllRecordings": "Show all recordings",
"showStarredRecordings": "Show starred recordings only"
"showStarredRecordings": "Show starred recordings only",
"synchronisedPlayer": "Synchronised player",
"synchronisedPlayerHint": "Play all recordings of the selected region, one player per camera",
"synchronisedPlayerRequirement": "Select a time region of maximum 60 minutes on the timeline to enable the synchronised player",
"synchronisedPlayerPlay": "Play",
"synchronisedPlayerPause": "Pause",
"synchronisedPlayerNoFootage": "No recording at this moment",
"synchronisedPlayerEmpty": "No playable recordings found in the selected time region.",
"timelinePreviewPlayVideo": "Play video"
},
"emptyState": {
"selectDayAndDevice": "Select day and device",
@@ -586,8 +998,12 @@
"timeline": {
"title": "Timeline",
"clearSelectedTime": "Clear selected time",
"selectionStart": "Start of the selected time range",
"selectionEnd": "End of the selected time range",
"fetching": "Fetching timeline rows for selected devices.",
"noRecordings": "No recordings are available for the selected date and devices."
"noRecordings": "No recordings are available for the selected date and devices.",
"resizeHint": "Drag to resize the timeline",
"rowHeight": "Drag to change the height of the recording bars"
},
"chart": {
"title": "Chart",
@@ -608,8 +1024,13 @@
"copyVideoUrl": "Copy Video URL",
"verifyVideo": "Verify video",
"shareRecording": "Share Recording",
"runWorkflow": "Run workflow",
"runWorkflowSubtitle": "Choose a workflow to run on this recording.",
"noMediaWorkflows": "No workflows are available for recordings.",
"newCase": "New case",
"addToCase": "Add to case",
"assignToCase": "Assign to case",
"caseActions": "Case actions",
"share": "Share",
"save": "Save",
"loading": "Loading...",
@@ -1126,7 +1547,7 @@
"k1": "Add a Dataset",
"k2": "Are you sure you want to remove device",
"k3": "Search dataset..",
"k4": "Set up your first Kerberos Agent",
"k4": "Set up your first Agent",
"k5": "No devices are available",
"k6": "Subscribe to a plan",
"k7": "Dataset",
@@ -1174,12 +1595,12 @@
},
"new": {
"new": {
"k1": "Before you will see your recordings shown up on Kerberos Hub, you first need to install and configure a Kerberos Agent. A Kerberos Agent is a piece of software that processes a device, and will make recordings when a portion of the video stream has changed. These recordings will then be send to, and processed by the Kerberos Hub application.",
"k2": "After you've properly installed one or more Kerberos Agents, you can link it to the Kerberos Hub application using your",
"k1": "Before you will see your recordings shown up on Kerberos Hub, you first need to install and configure an Agent. An Agent is a piece of software that processes a device, and will make recordings when a portion of the video stream has changed. These recordings will then be send to, and processed by the Kerberos Hub application.",
"k2": "After you've properly installed one or more Agents, you can link it to the Kerberos Hub application using your",
"k3": "cloud credentials",
"k4": ". When linked successfully, your device will show up on the devices page.",
"k5": "Get started",
"k6": "We have different ways to get your Kerberos Agent up and running. You can install the Kerberos Agent using a prebuilt Docker container, inside your Kubernetes Cluster, on your Raspberry Pi (or any other compatible board) or on a simple VM. Please select the most appropriate solution to get started. Visit",
"k6": "We have different ways to get your Agent up and running. You can install the Agent using a prebuilt Docker container, inside your Kubernetes Cluster, on your Raspberry Pi (or any other compatible board) or on a simple VM. Please select the most appropriate solution to get started. Visit",
"k7": "our documentation website to learn more",
"k8": "A Docker image (amd64, armv7, arm64) is",
"k9": "available on the Docker Hub",
@@ -1189,8 +1610,8 @@
"k13": "Go to the plan page",
"k14": "on your cloud account and copy your cloud credentials.",
"k15": "When created successfully, you can navigate to the Kerberos Factory interface, and enter your cloud credentials in your deployment of global configuration.",
"k16": "Your new Kerberos Agent will show up on the devices page.",
"k17": "If you want to manage Kerberos Agents at scale with Kubernetes, then",
"k16": "Your new Agent will show up on the devices page.",
"k17": "If you want to manage Agents at scale with Kubernetes, then",
"k18": "Kerberos Factory is what you are looking for",
"k19": "Setup a Kubernetes cluster or use your existing Kubernetes cluster.",
"k20": "Install the different components of",
@@ -1199,7 +1620,7 @@
"k23": "Go to the plans & integrations page",
"k24": "on your Kerberos Hub account and copy your credentials.",
"k25": "When configured Kerberos Factory successfully, you can modify the global settings, and enter your credentials.",
"k26": "All Kerberos Agent you will now add, will show up on your devices page.",
"k26": "All Agent you will now add, will show up on your devices page.",
"k27": "KiOS is a custom linux OS, created for the Raspberry Pi, which runs Kerberos.io out-of-the-box. This is the most simple and basic installation procedure.",
"k28": "Download",
"k29": "the KiOS installer",
@@ -1626,7 +2047,7 @@
"k11": "Delete attachment",
"k12": "Are you sure you want to delete this attachment? This action cannot be undone.",
"k13": "Yes, delete",
"k14": "Redact faces"
"k14": "Redact"
}
},
"media": {
@@ -1686,7 +2107,7 @@
"blocks": {
"face_redaction": {
"face_redaction": {
"k1": "Edit video",
"k1": "Redact video",
"k2": "Edit video tools",
"k3": "This editing tool is coming soon.",
"k4": "No detections available",
@@ -1702,9 +2123,7 @@
"k14": "fps",
"k15": "Detection run",
"k16": "Select a detection run",
"k17": "No detections",
"k18": "Automatic tracks unavailable",
"k19": "Automatic face tracks are disabled. You can still draw redaction boxes manually on the player and submit."
"k17": "No detections"
}
},
"motionmap": {
@@ -1930,13 +2349,15 @@
"k62": "Expiry date",
"k63": "Open update access token",
"k64": "No access tokens found",
"k65": "date, time",
"k65": "time",
"k66": "ip",
"k67": "browser",
"k68": "os",
"k67": "device",
"k68": "status",
"k69": "No login history found.",
"k70": "My account",
"k71": "Manage your profile, billing methods and invoices"
"k71": "Manage your profile, billing methods and invoices",
"k72": "Success",
"k73": "Failed"
}
},
"roles": {
@@ -2145,9 +2566,9 @@
"k20": "Kerberos Hub credentials",
"k21": "Cloud credentials",
"k22": "Media storage region",
"k23": "Your media can be stored in one of the following regions. Different Kerberos agents can store in different regions.",
"k23": "Your media can be stored in one of the following regions. Different Agents can store in different regions.",
"k24": "Copy",
"k25": "Use the below credentials to connect your Kerberos Agents to your Kerberos Hub.",
"k25": "Use the below credentials to connect your Agents to your Kerberos Hub.",
"k26": "Use the below credentials to connect your devices to our cloud.",
"k27": "How to connect Agents to Kerberos Hub",
"k28": "Public key",
@@ -2586,7 +3007,7 @@
"k2": "draft",
"k3": "Device detail",
"k4": "Kerberos Factory",
"k5": "Kerberos Agent",
"k5": "Agent",
"k6": "n/a",
"k7": "Remove device"
}
@@ -2822,7 +3243,7 @@
"k2": "PREVIEW",
"k3": "Switch to live [Shift + L]",
"k4": "LIVE",
"k5": "Make sure you are using the latest Kerberos agent and a Gold subscription",
"k5": "Make sure you are using the latest Agent and a Gold subscription",
"k6": "You are not allowed to watch a live stream.",
"k7": "Turn sound off",
"k8": "Turn sound on",
@@ -2834,7 +3255,22 @@
"k14": "Device Idle",
"k15": "The agent is online, but the device stream seems to be offline",
"k16": "No permissions",
"k17": "You aren't allowed to view this livestream."
"k17": "You aren't allowed to view this livestream.",
"k18": "Record",
"k19": "Recording",
"k20": "REC",
"k21": "Stop recording",
"k22": "Camera is set to continuous recording",
"k23": "Show motion detection",
"k24": "Hide motion detection",
"k25": "Recent recordings",
"k26": "No recordings available",
"k27": "Back 5 seconds",
"k28": "Play",
"k29": "Pause",
"k30": "Forward 5 seconds",
"k31": "Previous time window",
"k32": "Next time window"
}
},
"tags": {
@@ -2894,6 +3330,13 @@
"k3": "End time"
}
},
"time_spinner": {
"time_spinner": {
"hours": "Hours",
"minutes": "Minutes",
"seconds": "Seconds"
}
},
"video_js": {
"video_js": {
"k1": "To view this video please enable JavaScript, and consider upgrading to a web browser that",
@@ -0,0 +1,431 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"
/>
<meta name="description" content="Kerberos.io Mailing">
<style type="text/css">
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("https://kerberos.io/dist/fonts/Inter-Regular.woff?v=/dist/fonts/Inter-Regular.woff2?v=3.183.18") format("woff2"),
url("https://kerberos.io/dist/fonts/Inter-Regular.woff?v=/dist/fonts/Inter-Regular.woff2?v=3.183.18") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("https://kerberos.io/dist/fonts/Inter-Medium.woff2?v=3.18") format("woff2"),
url("https://kerberos.io/dist/fonts/Inter-Medium.woff?v=3.18") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("https://kerberos.io/dist/fonts/Inter-SemiBold.woff2?v=3.18") format("woff2"),
url("https://kerberos.io/dist/fonts/Inter-SemiBold.woff?v=3.18") format("woff");
}
@font-face {
font-family: 'Inter var';
font-weight: 100 900;
font-display: swap;
font-style: normal;
font-named-instance: 'Regular';
src: url("https://kerberos.io/dist/fonts/Inter-roman.var.woff2?v=3.18") format("woff2");
}
body{
background: #E5E5E5;
margin-top:0;
margin-bottom: 0;
margin-right: 0;
margin-left: 0;
padding-top: 0;
padding-left: 0;
padding-right: 0;
padding-bottom: 0;
font-family: 'Inter';
}
a, a:hover, a:active {
color: #262424;
text-decoration: none;
}
.corner-td{
width: 60px;
}
table {border-collapse:separate;max-width: 850px; margin: 0 auto; width: 100%;}
.ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td {line-height: 100%;}
.ExternalClass {width: 100%;}
@media screen and (max-width:500px){
.tab-td{
padding-left: 10px!important;
}
.tab-td a h4{
font-size: 14px!important;
}
.corner-td{
width: 20px!important;
}
.company-name-td h3{
font-size: 16px!important;
}
table.header-table{
padding-top: 8px!important;
padding-right: 0px!important;
padding-bottom: 24px!important;
padding-left: 0px!important;
}
.colored-card-td h4{
font-size: 14px!important;
}
.colored-card-td h2{
font-size: 20px!important;
}
.colored-card-td a p{
font-size: 12px!important;
width: 143px!important;
}
.colored-card-td{
padding-top: 24px!important;
padding-right: 24px!important;
padding-bottom: 24px!important;
padding-left: 24px!important;
}
.colorless-card-td{
padding-top: 24px!important;
padding-right: 24px!important;
padding-bottom: 24px!important;
padding-left: 24px!important;
}
.colorless-card-td h3{
font-size: 18px!important;
}
.colorless-card-td p{
font-size: 14px!important;
}
.colorless-card-table{
margin-left: 0px!important;
margin-right: 0px!important;
margin-top: 24px!important;
margin-bottom: 24px!important;
}
.footer-td{
display: table-row!important;
}
}
@media screen and (max-width:600px) {
.footer-td{
display: table-row!important;
}
}
@media screen and (max-width:650px) {
.footer-table{
margin-left: 0px!important;
margin-right: 0px!important;
margin-top: 0px!important;
margin-bottom: 36px!important;
}
}
</style>
</head>
<body height="100%" width="100%">
<table border="0" cellpadding="0" cellspacing="0" width="100%" bgcolor="E5E5E5" style="border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;">
<tr>
<td bgcolor="E5E5E5">
<table border="0" cellpadding="0" cellspacing="0" width="100%" height="36" class="header-table" style="padding-top: 36px ;padding-right: 0;padding-bottom: 36px;padding-left: 0;border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;">
<tbody>
<tr>
<td class="corner-td" align="left"></td>
<td width="48" height="36" align="left"><img alt="Kerberos.io" width="36" height="36" src="https://kerberos.io/images/email/kerberos.png"/></td>
<td height="36" align="left" class="company-name-td">
<h3 width="36" height="36" style=" font-family: Inter;
font-size: 20px;
font-style: normal;
font-weight: 600;
line-height: 24px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color: #262424;">Kerberos.io</h3>
</td>
<td height="36" width="36" style="padding-left: 36px;" class="tab-td" align="right">
<a style="text-decoration: none;color: none;" href={{tab1_href}}>
<h4 style="font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 36px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: right;
color: #6D6666;">{{tab1_title}}</h4>
</a>
</td>
<td height="36" width="36" style="padding-left: 36px;" class="tab-td" align="right" >
<a style="text-decoration: none;color: none;" href={{tab2_href}}>
<h4 style="font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 36px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: right;
color: #6D6666;">{{tab2_title}}</h4>
</a>
</td>
<td class="corner-td" align="right"></td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
<table border="0" cellpadding="0" cellspacing="0" width="100%" bgcolor="E5E5E5" style="border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;">
<tr>
<td bgcolor="E5E5E5">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;" >
<tbody>
<tr>
<td class="corner-td" align="left"></td>
<td class="colored-card-td" bgcolor="#57356B" style="padding-left: 48px;padding-right: 48px;padding-top: 48px;padding-bottom: 48px;border-radius: 4px;background-color:#57356B;">
<h4 style="font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color:#b09fb9;">Hey, {{user}}</h4>
<h2 style=" font-family: Inter;
font-size: 24px;
font-style: normal;
font-weight: 600;
line-height: 36px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color:#FFFFFF;
padding-top: 12px;
margin-bottom: 0;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;">Several of your cameras changed status</h2>
<h2 style=" font-family: Inter;
font-size: 20px;
font-style: normal;
font-weight: 600;
line-height: 36px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color:#FFFFFF;
padding-top: 0;
padding-bottom: 24px;
padding-left: 0;
padding-right: 0;
color:#b09fb9;">{{text}}</h2>
</td>
<td class="corner-td" align="right"></td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
<table border="0" cellpadding="0" cellspacing="0" width="100%" bgcolor="E5E5E5" style="border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;">
<tr>
<td bgcolor="E5E5E5">
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="colorless-card-table" style="margin-top: 36px;margin-bottom: 36px;margin-left: 0;margin-right: 0;border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;" >
<tbody>
<tr>
<td class="corner-td" align="left"></td>
<td class="colorless-card-td" bgcolor="#FFFFFF" style="background-color:#FFFFFF;padding-top: 36px;padding-right: 43px;padding-bottom: 25px;padding-left: 43px;border-radius: 4px;">
<h3 style=" font-family: Inter;
font-size: 20px;
font-style: normal;
font-weight: 600;
line-height: 36px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color: #262424;
width: 480px">What changed</h3>
{{sections}}
<p style=" font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 24px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color: #6D6666;
margin-top: 12px;">Cameras that go offline together usually share a cause, such as a site losing its internet connection or power. Have a look into your Kerberos Hub account for the latest recordings and the status of your cameras.</p>
<a style="text-decoration: none;color: none;" href="{{link}}">
<p style="font-family: Inter;
font-size: 14px;
font-style: normal;
line-height: 24px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color:#FFFFFF;
background-color: #84559F;
padding-top: 6px;
padding-bottom: 6px;
padding-right: 16px;
padding-left: 16px;
width: 160px;
border-radius: 4px;
text-align: center;
cursor: pointer;">Go to Kerberos Hub -></p>
</a>
</td>
<td class="corner-td" align="right"></td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
<table border="0" cellpadding="0" cellspacing="0" width="100%" bgcolor="E5E5E5" style="border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;">
<tr>
<td bgcolor="E5E5E5">
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="footer-table" style="margin-top: 0;border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;" >
<tbody>
<tr>
<td class="corner-td" align="left"></td>
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:top;display:table-row !important">
<![endif]-->
<td height="146" width="190" class="footer-td" style="margin-bottom: 12px;" valign="top" align="left">
<table style="border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;">
<tbody>
<tr>
<td>
<h4 style=" font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 36px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color:#6D6666;">Get in touch</h4>
<a style="text-decoration: none;color: none;" href="mailto:support@verstraeten.io">
<p style=" font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 16px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color: #A69D9D;">support@kerberos.io</p>
</a>
<p style=" font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 16px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color: #A69D9D;">9000 Ghent, BE</p>
<a style="text-decoration: none;color: none;" href="https://kerberos.io/">
<p style=" font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 24px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color: #A69D9D;">https://kerberos.io</p>
</a>
</td>
</tr>
</tbody>
</table>
</td>
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:top;display:table-row !important">
<![endif]-->
<td class="footer-td" style="border-radius: 4px;padding-left: 0;padding-right: 0;padding-top: 0;padding-bottom: 0; margin-bottom: 12px;" valign="top" align="left">
<table style="border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;">
<tbody>
<tr>
<td>
<h4 style=" font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 36px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color:#6D6666;">About Kerberos</h4>
<p style=" font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 24px;
mso-line-height-rule:exactly;
letter-spacing: 0em;
text-align: left;
color: #A69D9D;">Welcome to the revolutionary video analytics and video management platform. Open, modular, and extensible for everyone, anywhere.</p>
<p style="margin-top: 12px;">
<a href="https://twitter.com/kerberosio" style="text-decoration: none;color: none;">
<img width="24" height="24" alt="Twitter" src="https://kerberos.io/images/email/twitter.png"/>
</a>
<a href="https://reddit.com/r/kerberos_io" style="text-decoration: none;color: none;">
<img g width="24" height="24" alt="Reddit" src="https://kerberos.io/images/email/reddit.png"/>
</a>
<a href="https://www.youtube.com/channel/UCnd9q7iRNNw4W95eQwQuECA" style="text-decoration: none;color: none;">
<img g width="24" height="24" alt="Youtube" src="https://kerberos.io/images/email/youtube.png"/>
</a>
<a href="https://github.com/kerberos-io" style="text-decoration: none;color: none;">
<img g width="24" height="24" alt="Github" src="https://kerberos.io/images/email/github.png"/>
</a>
</p>
</td>
</tr>
</tbody>
</table>
</td>
<td class="corner-td" align="right"></td>
</tr>
</tbody>
</table>
<table style="border-collaps:collaps; mso-table-lspace:0pt; mso-table-rspace:0pt;">
<tbody>
<tr style="height: 50px">
<td></td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</body>
</html>
@@ -0,0 +1,22 @@
Kerberos.io
------------
Hey, {{user}}
Several of your cameras changed status
{{text}}
{{sectionsText}}
Go to Kerberos Hub ->
{{link}}
Get in touch
------------
support@kerberos.io
9000 Ghent, BE
https://kerberos.io
About Kerberos
------------
Welcome to the revolutionary video analytics and video management platform. Open, modular, and extensible for everyone, anywhere.
@@ -0,0 +1,24 @@
# Create these ConfigMaps in the release namespace before deploying the chart.
# Keys contain complete contracts/catalogs; they are not merged with chart data.
# Do not also set classificationCatalog or classificationCatalogExistingConfigMap.
classificationCatalogEnabled: true
workflowConfiguration:
enabled: true
includeDefaults: true
configMaps:
- name: deployment-workflow-overrides
items:
- key: start.yaml
path: workflow-contracts/start.yaml
replace: true
- key: deployment-classifications.json
path: catalogs/classifications.json
replace: true
- name: custom-workflow-contracts
items:
- key: pose.yaml
path: workflow-contracts/pose.yaml
- name: shared-pose-catalog
items:
- key: pose-keypoints.json
path: catalogs/pose-keypoints.json
@@ -1,9 +1,13 @@
# Packaging only; does not activate contract loading or deploy a pose worker.
workflowConfiguration:
enabled: true
existingConfigMap: custom-workflow-configuration
items:
- key: pose.yaml
path: workflow-contracts/pose.yaml
- key: pose-keypoints.json
path: catalogs/pose-keypoints.json
includeDefaults: true
configMaps:
- name: custom-workflow-contracts
items:
- key: pose.yaml
path: workflow-contracts/pose.yaml
- name: shared-pose-catalog
items:
- key: pose-keypoints.json
path: catalogs/pose-keypoints.json
@@ -3,6 +3,23 @@
schemaVersion: 1
stage: pose
contractVersion: 1
presentation:
name: Detect poses
description: Estimate body keypoints and posture for people in the recording.
icon: person
color: teal
# Settings authors enter per node; the worker receives them as node data.
params:
- name: minConfidence
label: Minimum keypoint confidence
type: number
default: 0.5
- name: model
label: Model
type: select
options: [fast, accurate]
optionLabels: {fast: Fast, accurate: Accurate}
default: fast
fields:
- id: keypoint
label: Keypoint
@@ -25,3 +42,12 @@ fields:
operators: [gt, gte, lt, lte]
minimum: 0
maximum: 1
# Connections leaving this stage: one section with fixed inputs, then the
# Extra conditions catch-all (appended collapsed when not placed explicitly).
panel:
sections:
- id: posture
label: Posture
fields:
- {field: posture, widget: multiselect}
- {field: confidence, widget: number, operator: gte, label: Minimum confidence}
+196 -17
View File
@@ -1,6 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Hub workflow condition contract v1 (preparatory)",
"title": "Hub workflow editor contract v1",
"type": "object",
"additionalProperties": false,
"required": ["schemaVersion", "stage", "contractVersion", "fields"],
@@ -11,11 +11,32 @@
"pattern": "^[a-z][a-z0-9-]*$",
"description": "Stage type, not a node instance. start describes initial context."
},
"contractVersion": {"type": "integer", "minimum": 1},
"contractVersion": {"const": 1},
"presentation": {
"type": "object",
"additionalProperties": false,
"description": "How the editor shows this node. Icons and colours come from approved sets.",
"properties": {
"name": {"type": "string", "minLength": 1, "maxLength": 80},
"description": {"type": "string", "minLength": 1, "maxLength": 500},
"icon": {"enum": ["play", "camera", "person", "eye-crossed", "car", "clock", "mlModel", "share"]},
"color": {
"enum": ["blue", "teal", "rose", "amber", "indigo", "violet", "green", "grey"],
"description": "Theme colour token."
}
}
},
"params": {
"type": "array",
"maxItems": 64,
"description": "Worker-stage settings authors enter, stored per node. An explicit list (even empty) replaces kerberoshub.workflows.editorStages params. Not supported on start.",
"items": {"$ref": "#/definitions/param"}
},
"fields": {
"type": "array",
"minItems": 1,
"items": {"$ref": "#/definitions/field"}
"description": "Typed values conditions may test. Start: the selectors device, sites, groups and classifications plus condition-only fields whose path exists in the condition root schema (models.WorkflowConditionRootSchema), e.g. device.deviceName or inputs.classify.details.*.isStatic; other inputs.<operation> paths are worker-defined. Stage: outputs relative to results.<stage>. New conditions may only use declared fields. An empty array is valid.",
"items": {"$ref": "#/definitions/field"},
"maxItems": 128
},
"controls": {
"type": "array",
@@ -27,8 +48,18 @@
"id": {"enum": ["weeklySchedule", "conditions"]},
"label": {"type": "string", "minLength": 1}
}
}
}
},
"description": "Start only. Controls offered for new authoring; omitted offers none. conditions applies to edges leaving Start.",
"maxItems": 16
},
"triggerModes": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {"enum": ["automatic", "manual"]},
"description": "Start only. Trigger modes offered for new authoring; omitted offers both."
},
"panel": {"$ref": "#/definitions/panel"}
},
"definitions": {
"choices": {
@@ -39,9 +70,15 @@
"type": "string",
"description": "Built-in authorized project-resource resolver; not a database path.",
"oneOf": [
{"const": "project.devices", "description": "Device keys as values, device names as labels."},
{
"const": "project.devices",
"description": "Device keys as values, device names as labels."
},
{"const": "project.sites", "description": "Site IDs as values, site names as labels."},
{"const": "project.groups", "description": "Group IDs as values, group names as labels."}
{
"const": "project.groups",
"description": "Group IDs as values, group names as labels."
}
]
},
"file": {
@@ -56,11 +93,7 @@
"items": {"type": ["string", "number", "boolean"]}
}
},
"oneOf": [
{"required": ["source"]},
{"required": ["file"]},
{"required": ["values"]}
]
"oneOf": [{"required": ["source"]}, {"required": ["file"]}, {"required": ["values"]}]
},
"field": {
"type": "object",
@@ -131,12 +164,158 @@
{
"if": {"properties": {"type": {"enum": ["integer", "number", "boolean"]}}},
"then": {
"properties": {
"choices": {"not": {"anyOf": [{"required": ["file"]}, {"required": ["source"]}]}}
}
"properties": {"choices": {"not": {"anyOf": [{"required": ["file"]}, {"required": ["source"]}]}}}
}
}
]
},
"param": {
"type": "object",
"additionalProperties": false,
"required": ["name", "type"],
"properties": {
"name": {
"type": "string",
"pattern": "^[A-Za-z][A-Za-z0-9_]*$",
"maxLength": 64,
"description": "Key in node data; unique within the contract."
},
"label": {"type": "string", "minLength": 1, "maxLength": 160},
"description": {"type": "string", "minLength": 1, "maxLength": 500},
"group": {
"type": "string",
"minLength": 1,
"maxLength": 80,
"description": "Editor section; params sharing a group appear together."
},
"placeholder": {"type": "string", "minLength": 1, "maxLength": 160},
"type": {"enum": ["string", "number", "boolean", "select", "multiselect", "secret"]},
"required": {"type": "boolean"},
"default": {
"description": "Value the editor pre-fills and the API stores when a node leaves the setting unset; never for secrets. Typically the worker's own default."
},
"options": {
"type": "array",
"minItems": 1,
"maxItems": 256,
"uniqueItems": true,
"items": {"type": "string", "minLength": 1, "maxLength": 160}
},
"optionLabels": {
"type": "object",
"additionalProperties": {"type": "string", "minLength": 1, "maxLength": 160}
},
"minimum": {"type": "number", "description": "Inclusive lower bound for a number setting."},
"maximum": {"type": "number", "description": "Inclusive upper bound for a number setting."},
"choices": {
"type": "object",
"additionalProperties": false,
"required": ["file"],
"properties": {
"file": {"type": "string", "pattern": "^\\.\\./catalogs/[A-Za-z0-9_-][A-Za-z0-9._-]*\\.json$"}
},
"description": "Options for a select or multiselect setting from a shared catalog file (keys become options, labels become optionLabels). Use instead of options."
}
},
"allOf": [
{
"if": {"properties": {"type": {"enum": ["select", "multiselect"]}}},
"then": {"oneOf": [{"required": ["options"]}, {"required": ["choices"]}]},
"else": {
"not": {
"anyOf": [{"required": ["options"]}, {"required": ["optionLabels"]}, {"required": ["choices"]}]
}
}
},
{
"if": {"properties": {"type": {"const": "secret"}}},
"then": {"not": {"required": ["default"]}}
},
{
"if": {"properties": {"type": {"enum": ["string", "select"]}}},
"then": {"properties": {"default": {"type": "string"}}}
},
{
"if": {"properties": {"type": {"const": "multiselect"}}},
"then": {
"properties": {"default": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}}
}
},
{
"if": {"properties": {"type": {"const": "number"}}},
"then": {"properties": {"default": {"type": "number"}}},
"else": {"not": {"anyOf": [{"required": ["minimum"]}, {"required": ["maximum"]}]}}
},
{
"if": {"properties": {"type": {"const": "boolean"}}},
"then": {"properties": {"default": {"type": "boolean"}}}
}
]
},
"panel": {
"type": "object",
"additionalProperties": false,
"required": ["sections"],
"description": "Layout of connections leaving this node. Without it, a default panel is used. A section with picker: true positions the Extra conditions catch-all; without one it is appended collapsed. On start, a panel replaces the top-level controls list.",
"properties": {
"sections": {
"type": "array",
"minItems": 1,
"maxItems": 16,
"items": {"$ref": "#/definitions/panelSection"}
}
}
},
"panelSection": {
"type": "object",
"additionalProperties": false,
"required": ["id", "label"],
"properties": {
"id": {"type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_-]*$", "maxLength": 80},
"label": {"type": "string", "minLength": 1, "maxLength": 160},
"collapsed": {"type": "boolean"},
"picker": {
"type": "boolean",
"description": "This section is the Extra conditions catch-all over every declared field of Start and upstream stages; it holds no fields or controls."
},
"controls": {
"type": "array",
"items": {"const": "weeklySchedule"},
"maxItems": 1,
"description": "Start only."
},
"fields": {"type": "array", "maxItems": 32, "items": {"$ref": "#/definitions/panelField"}}
},
"if": {"properties": {"picker": {"const": true}}, "required": ["picker"]},
"then": {"not": {"anyOf": [{"required": ["fields"]}, {"required": ["controls"]}]}}
},
"panelField": {
"type": "object",
"additionalProperties": false,
"required": ["field", "widget"],
"properties": {
"field": {
"type": "string",
"description": "ID of a field declared in this contract; each field is placed at most once."
},
"label": {"type": "string", "minLength": 1, "maxLength": 160},
"widget": {
"enum": ["multiselect", "select", "text", "number", "toggle"],
"description": "multiselect: string with choices (in); select: choices (eq, ne); text: string (eq, contains, matches, ne, in); number: integer or number (gte, gt, lte, lt, eq, ne); toggle: boolean (eq, ne)."
},
"operator": {
"enum": ["eq", "ne", "in", "contains", "matches", "gt", "gte", "lt", "lte"],
"description": "Fixed operator; must be declared by the field and supported by the widget. Defaults to the first widget operator the field declares."
}
}
}
}
},
"allOf": [
{
"if": {"properties": {"stage": {"const": "start"}}, "required": ["stage"]},
"then": {"not": {"required": ["params"]}},
"else": {"not": {"anyOf": [{"required": ["controls"]}, {"required": ["triggerModes"]}]}}
},
{"if": {"required": ["panel"]}, "then": {"not": {"required": ["controls"]}}}
]
}
+106 -19
View File
@@ -1,27 +1,121 @@
{{/* Keep the legacy endpoint and the preparatory bundle on the same chart data. */}}
{{- define "hub.classificationCatalog" -}}
{{- if kindIs "invalid" .Values.classificationCatalog -}}
{{- .Files.Get "configuration/catalogs/classifications.json" | mustFromJson | toPrettyJson -}}
{{- else if kindIs "slice" .Values.classificationCatalog -}}
{{- .Values.classificationCatalog | toPrettyJson -}}
{{- define "hub.workflowConfiguration.bundledPaths" -}}
{{- list "workflow-contracts/start.yaml" "workflow-contracts/anpr.yaml" "workflow-contracts/forwarder.yaml" "catalogs/classifications.json" | toYaml -}}
{{- end -}}
{{- define "hub.workflowConfiguration.anpr.configMapName" -}}
{{- printf "%s-workflow-contract-anpr" (.Release.Name | trunc 40 | trimSuffix "-") -}}
{{- end -}}
{{- define "hub.workflowConfiguration.forwarder.configMapName" -}}
{{- printf "%s-workflow-contract-forwarder" (.Release.Name | trunc 40 | trimSuffix "-") -}}
{{- end -}}
{{- define "hub.classificationCatalog.configMapName" -}}
{{- $config := include "hub.workflowConfiguration.settings" . | fromYaml -}}
{{- $replacement := index $config.replacements "catalogs/classifications.json" | default dict -}}
{{- if $replacement -}}
{{- $replacement.name -}}
{{- else -}}
{{- fail "classificationCatalog must be null (bundled defaults) or a list" -}}
{{- default "classification-catalog" .Values.classificationCatalogExistingConfigMap -}}
{{- end -}}
{{- end -}}
{{- define "hub.workflowConfiguration.validate" -}}
{{/* Normalize both interfaces without changing the caller's values. */}}
{{- define "hub.workflowConfiguration.settings" -}}
{{- $config := .Values.workflowConfiguration | default dict -}}
{{- if $config.enabled -}}
{{- $unified := or (hasKey $config "includeDefaults") (hasKey $config "configMaps") -}}
{{- $legacy := or (hasKey $config "existingConfigMap") (hasKey $config "items") (hasKey $config "extraConfigMaps") -}}
{{- if and $unified $legacy -}}
{{- fail "workflowConfiguration cannot mix includeDefaults/configMaps with existingConfigMap/items/extraConfigMaps" -}}
{{- end -}}
{{- $includeDefaults := true -}}
{{- $sources := $config.extraConfigMaps | default list -}}
{{- if $unified -}}
{{- $sources = $config.configMaps | default list -}}
{{- if hasKey $config "includeDefaults" -}}
{{- $includeDefaults = $config.includeDefaults -}}
{{- end -}}
{{- else -}}
{{- if $config.existingConfigMap -}}
{{- if not $config.items -}}
{{- if and $config.enabled (not $config.items) -}}
{{- fail "workflowConfiguration.items is required with existingConfigMap" -}}
{{- end -}}
{{- $includeDefaults = false -}}
{{- $sources = prepend $sources (dict "name" $config.existingConfigMap "items" ($config.items | default list)) -}}
{{- else if and $config.enabled $config.items -}}
{{- fail "workflowConfiguration.items requires existingConfigMap" -}}
{{- end -}}
{{- end -}}
{{- $enabled := $config.enabled | default false -}}
{{- $replacements := dict -}}
{{- $normalizedSources := list -}}
{{- $bundledPaths := include "hub.workflowConfiguration.bundledPaths" . | fromYamlArray -}}
{{- range $sources -}}
{{- $name := .name -}}
{{- $items := list -}}
{{- range .items -}}
{{- if and $enabled .replace -}}
{{- if or (not $unified) (not $includeDefaults) -}}
{{- fail "workflowConfiguration item replace: true requires configMaps with includeDefaults: true" -}}
{{- end -}}
{{- if not (has .path $bundledPaths) -}}
{{- fail (printf "workflowConfiguration item replace: true targets unknown bundled path %q" .path) -}}
{{- end -}}
{{- if hasKey $replacements .path -}}
{{- fail (printf "duplicate workflowConfiguration replacement path %q" .path) -}}
{{- end -}}
{{- $_ := set $replacements .path (dict "name" $name "key" .key) -}}
{{- end -}}
{{- $items = append $items (omit . "replace") -}}
{{- end -}}
{{- $normalizedSources = append $normalizedSources (dict "name" $name "items" $items) -}}
{{- end -}}
{{- if hasKey $replacements "catalogs/classifications.json" -}}
{{- if .Values.classificationCatalogExistingConfigMap -}}
{{- fail "a classifications.json replacement cannot be combined with classificationCatalogExistingConfigMap; choose one override mechanism" -}}
{{- end -}}
{{- end -}}
{{- if not (kindIs "invalid" .Values.classificationCatalog) -}}
{{- if not .Values.classificationCatalogEnabled -}}
{{- fail "classificationCatalog requires classificationCatalogEnabled; remove the inline list to use the API fallback" -}}
{{- end -}}
{{- if .Values.classificationCatalogExistingConfigMap -}}
{{- fail "classificationCatalog cannot be combined with classificationCatalogExistingConfigMap; choose one override mechanism" -}}
{{- end -}}
{{- if hasKey $replacements "catalogs/classifications.json" -}}
{{- fail "classificationCatalog cannot be combined with a classifications.json replacement; choose one override mechanism" -}}
{{- end -}}
{{- end -}}
{{- dict "enabled" $enabled "includeDefaults" $includeDefaults "configMaps" $normalizedSources "replacements" $replacements | toYaml -}}
{{- end -}}
{{- define "hub.workflowConfiguration.validate" -}}
{{- $config := include "hub.workflowConfiguration.settings" . | fromYaml -}}
{{- if $config.enabled -}}
{{- $paths := dict -}}
{{- range $config.items -}}
{{- if $config.includeDefaults -}}
{{- if not .Values.classificationCatalogEnabled -}}
{{- fail "bundled workflowConfiguration requires classificationCatalogEnabled; disable includeDefaults and supply configMaps for a custom bundle" -}}
{{- end -}}
{{- range $path := (include "hub.workflowConfiguration.bundledPaths" . | fromYamlArray) -}}
{{- if not (hasKey $config.replacements $path) -}}
{{- $_ := set $paths $path true -}}
{{- end -}}
{{- end -}}
{{- else -}}
{{- if not $config.configMaps -}}
{{- fail "workflowConfiguration.configMaps must be nonempty when enabled without defaults" -}}
{{- end -}}
{{- end -}}
{{- range $config.configMaps -}}
{{- if not .items -}}
{{- fail (printf "workflowConfiguration ConfigMap %q requires nonempty items" .name) -}}
{{- end -}}
{{- range .items -}}
{{- if not (regexMatch "^[A-Za-z0-9._-]+$" .key) -}}
{{- fail "workflowConfiguration.items keys must be valid ConfigMap keys" -}}
{{- end -}}
{{- if or (not (regexMatch "^(workflow-contracts|catalogs)/[A-Za-z0-9._/-]+$" .path)) (ne (clean .path) .path) -}}
{{- if or (not (regexMatch "^(workflow-contracts|catalogs)/[A-Za-z0-9_-][A-Za-z0-9._-]*\\.(yaml|json)$" .path)) (ne (clean .path) .path) -}}
{{- fail "workflowConfiguration.items paths must be normalized relative files under workflow-contracts/ or catalogs/" -}}
{{- end -}}
{{- if hasKey $paths .path -}}
@@ -29,13 +123,6 @@
{{- end -}}
{{- $_ := set $paths .path true -}}
{{- end -}}
{{- else -}}
{{- if $config.items -}}
{{- fail "workflowConfiguration.items requires existingConfigMap" -}}
{{- end -}}
{{- if not .Values.classificationCatalogEnabled -}}
{{- fail "bundled workflowConfiguration requires classificationCatalogEnabled; use an existingConfigMap for a custom bundle" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
+51
View File
@@ -0,0 +1,51 @@
{{/*
The system tag catalog (deviceTags.system.catalog) uses the Hub shared choice
catalog format: {key, label, description?}. values.schema.json validates each
entry (key in stored device tag form, models.NormalizeDeviceTags: 1-64
characters of a-z, 0-9, ".", "_", ":" and "-", starting and ending with a
letter or digit) and that enabled is a boolean; keys must also be unique.
*/}}
{{- define "kerberoshub.deviceTags.system.validate" -}}
{{- $seen := dict -}}
{{- range $tag := (include "kerberoshub.deviceTags.system.catalog" . | fromJsonArray) -}}
{{- $key := toString $tag.key -}}
{{- if hasKey $seen $key -}}
{{- fail (printf "deviceTags.system.catalog: duplicate key %q" $key) -}}
{{- end -}}
{{- $_ := set $seen $key true -}}
{{- end -}}
{{- end -}}
{{/*
The configured system tag catalog as a JSON array, whether or not system tags
are enabled (missing values read as empty).
*/}}
{{- define "kerberoshub.deviceTags.system.catalog" -}}
{{- $deviceTags := .Values.deviceTags | default dict -}}
{{- $system := $deviceTags.system | default dict -}}
{{- $system.catalog | default list | toJson -}}
{{- end -}}
{{/*
Whether system tags are enabled (deviceTags.system.enabled): "true" or "".
*/}}
{{- define "kerberoshub.deviceTags.system.enabled" -}}
{{- $deviceTags := .Values.deviceTags | default dict -}}
{{- $system := $deviceTags.system | default dict -}}
{{- if and (kindIs "bool" $system.enabled) $system.enabled -}}true{{- end -}}
{{- end -}}
{{/*
The system tag catalog file hub-api reads (DEVICE_SYSTEM_TAGS_FILE), rendered
from deviceTags.system.catalog into the device-system-tags ConfigMap.
*/}}
{{- define "kerberoshub.deviceTags.system.json" -}}
{{- include "kerberoshub.deviceTags.system.validate" . -}}
{{- $tags := list -}}
{{- range $tag := (include "kerberoshub.deviceTags.system.catalog" . | fromJsonArray) -}}
{{- $entry := dict "key" $tag.key "label" $tag.label -}}
{{- with $tag.description }}{{- $_ := set $entry "description" . -}}{{- end }}
{{- $tags = append $tags $entry -}}
{{- end -}}
{{- $tags | toJson -}}
{{- end -}}
+25 -1
View File
@@ -81,4 +81,28 @@ Usage: {{- include "hub.mongodb.appUriEnv" (dict "root" $ "app" "hub-api") | nin
- name: mongodb-tls
mountPath: {{ .Values.mongodb.tls.mountPath }}
readOnly: true
{{- end -}}
{{- end -}}
{{/*
OpenTelemetry sampling env.
"hub.opentelemetry.samplerEnv" is for the services that only continue traces
started elsewhere (hub-workflows, pipe-*, workflow stages): they follow the
parent's sampling decision and never start traces of their own.
"hub.opentelemetry.apiSamplerEnv" renders hub-api's per-class sampling ratios
(opentelemetry.apiSamplingRatios) as TRACE_SAMPLER_RATIO_<CLASS>; empty values
are not rendered.
Usage: {{- include "hub.opentelemetry.samplerEnv" $ | trim | nindent 8 }}
*/}}
{{- define "hub.opentelemetry.samplerEnv" -}}
- name: OTEL_TRACES_SAMPLER
value: "parentbased_always_off"
{{- end -}}
{{- define "hub.opentelemetry.apiSamplerEnv" -}}
{{- range $class, $ratio := (.Values.opentelemetry.apiSamplingRatios | default dict) }}
{{- if ne (toString $ratio) "" }}
- name: TRACE_SAMPLER_RATIO_{{ upper $class }}
value: {{ toString $ratio | quote }}
{{- end }}
{{- end }}
{{- end -}}
+13 -1
View File
@@ -26,6 +26,10 @@ spec:
- name: admin
image: "{{ .Values.global.imageRegistry }}{{ .Values.admin.repository }}:{{ .Values.admin.tag }}"
imagePullPolicy: {{ .Values.admin.pullPolicy }}
{{- with .Values.admin.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.admin.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -36,8 +40,16 @@ spec:
{{- end }}
ports:
- name: http
containerPort: 80
containerPort: {{ .Values.admin.containerPort | default 80 }}
protocol: TCP
{{- with .Values.admin.readinessProbe }}
readinessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.admin.livenessProbe }}
livenessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
env:
- name: LOG_LEVEL
value: "{{ .Values.admin.logLevel }}"
+1 -1
View File
@@ -10,7 +10,7 @@ spec:
ports:
- protocol: TCP
port: 80
targetPort: 80
targetPort: http
name: http
selector:
app: admin
@@ -0,0 +1,46 @@
{{- define "hub.classificationCatalog.defaults" -}}
[
{"key": "animal", "label": "Animal", "icon": "animal"},
{"key": "pedestrian", "label": "Pedestrian", "icon": "pedestrian"},
{"key": "cyclist", "label": "Cyclist", "icon": "cyclist"},
{"key": "motorbike", "label": "Motorbike", "icon": "motorbike"},
{"key": "lorry", "label": "Lorry", "icon": "lorry"},
{"key": "car", "label": "Car", "icon": "car"},
{"key": "handbag", "label": "Handbag", "icon": "handbag"},
{"key": "suitcase", "label": "Suitcase", "icon": "suitcase"},
{"key": "cell phone", "label": "Cell phone", "icon": "cell phone"}
]
{{- end -}}
{{/*
The effective chart-rendered catalog: the inline classificationCatalog values
list when set, otherwise the bundled defaults above. values.schema.json
validates each entry ({key, label, icon?}); keys must also be unique.
*/}}
{{- define "hub.classificationCatalog.json" -}}
{{- if kindIs "invalid" .Values.classificationCatalog -}}
{{- include "hub.classificationCatalog.defaults" . | mustFromJson | toJson -}}
{{- else if and (kindIs "slice" .Values.classificationCatalog) .Values.classificationCatalog -}}
{{- $seen := dict -}}
{{- range $entry := .Values.classificationCatalog -}}
{{- $key := toString $entry.key -}}
{{- if hasKey $seen $key -}}
{{- fail (printf "classificationCatalog: duplicate key %q" $key) -}}
{{- end -}}
{{- $_ := set $seen $key true -}}
{{- end -}}
{{- .Values.classificationCatalog | toJson -}}
{{- else -}}
{{- fail "classificationCatalog must be omitted (bundled defaults) or a nonempty list; to use the API fallback set classificationCatalogEnabled: false" -}}
{{- end -}}
{{- end -}}
{{- $config := include "hub.workflowConfiguration.settings" . | fromYaml -}}
{{- if and .Values.classificationCatalogEnabled (not .Values.classificationCatalogExistingConfigMap) (not (hasKey $config.replacements "catalogs/classifications.json")) }}
apiVersion: v1
kind: ConfigMap
metadata:
name: classification-catalog
namespace: {{ .Release.Namespace }}
data:
classifications.json: |-
{{- include "hub.classificationCatalog.json" . | mustFromJson | toPrettyJson | nindent 4 }}
{{- end -}}
@@ -0,0 +1,12 @@
{{- if include "kerberoshub.deviceTags.system.enabled" . }}
apiVersion: v1
kind: ConfigMap
metadata:
name: device-system-tags
namespace: {{ .Release.Namespace }}
data:
# Rendered from deviceTags.system.catalog; hub-api reads it through
# DEVICE_SYSTEM_TAGS_FILE.
system-tags.json: |-
{{- include "kerberoshub.deviceTags.system.json" . | mustFromJson | toPrettyJson | nindent 4 }}
{{- end -}}
@@ -0,0 +1,135 @@
{{/* Validate the rendered anpr.yaml payload against schemas/workflow-contract.schema.json in the chart root. */}}
{{- define "hub.workflowConfiguration.anpr" -}}
schemaVersion: 1
stage: anpr
contractVersion: 1
presentation:
name: Number plate recognition
description: Read the number plates of vehicles in the recording.
icon: car
color: amber
# Node settings hub-anpr reads per run (see its README, "Node settings"). Each
# overrides the matching ANPR_* environment variable; the defaults below equal
# the worker's built-in defaults. A deployment that changes those variables
# should change these defaults too, because new nodes store them on save.
params:
- name: plateClasses
label: Read plates for
description: Vehicle classes whose plates are read (ANPR_PLATE_CLASSES).
type: multiselect
choices: {file: ../catalogs/classifications.json}
default: [car]
group: Which vehicles
- name: minMovement
label: Minimum movement
description: Skip vehicles that barely moved; 0 reads every vehicle (ANPR_READ_MOVEMENT_MIN).
type: number
minimum: 0
default: 0
group: Which vehicles
- name: maxJump
label: Maximum jump between frames
description: Drop unstable tracks; 0 disables the check (ANPR_READ_MAX_JUMP).
type: number
minimum: 0
default: 0
group: Which vehicles
- name: readFrames
label: Frames to read per vehicle
description: Frames OCR'd per vehicle; 0 disables OCR (ANPR_READ_FRAMES).
type: number
minimum: 0
maximum: 10
default: 1
group: Recognition
- name: readQuorum
label: Agreeing reads required
description: Reads that must agree before a plate is accepted; 0 accepts any read (ANPR_READ_QUORUM).
type: number
minimum: 0
maximum: 10
default: 0
group: Recognition
- name: minConfidence
label: Minimum OCR confidence
description: Ignore reads below this confidence (ANPR_READ_CONF_MIN).
type: number
minimum: 0
maximum: 1
default: 0
group: Recognition
# Paths are relative to results.anpr (models.WorkflowAnprResult): one track and
# one marker per vehicle. Plate, unread and class can be tied to the same
# vehicle with "Must match the same item"; OCR confidence and engine live on
# the markers and cannot be tied to a plate.
fields:
- id: plate
label: Number plate
path: detections.*.tracks.*.meta.plate
type: string
operators: [eq, in, contains, matches] # stored uppercase, letters and digits only, e.g. AB123CD
optional: true
- id: unread
label: Plate could not be read
path: detections.*.tracks.*.meta.unread
type: boolean
operators: [eq]
optional: true
- id: vehicleClass
label: Vehicle class (from classification)
path: detections.*.tracks.*.meta.classifiedAs
type: string
operators: [eq, in]
optional: true
choices: {file: ../catalogs/classifications.json}
- id: ocrConfidence
label: OCR confidence
path: markers.*.metadata.confidence
type: number
operators: [gte, gt, lte, lt] # read plates only; about 0-1 for fast, 0.5 for tesseract
minimum: 0
optional: true
- id: ocrEngine
label: OCR engine
path: markers.*.metadata.engine
type: string
operators: [eq, in] # read plates only
optional: true
choices: {values: [fast, tesseract, tesseract+opencv, http]}
- id: secondsVisible
label: Seconds visible
path: markers.*.duration
type: integer
operators: [gte, lte]
minimum: 0
optional: true
# Connections leaving this stage; Extra conditions are appended collapsed.
panel:
sections:
- id: plate
label: Plate
fields:
- {field: plate, widget: text, operator: contains, label: Plate contains (e.g. AB123)}
- {field: unread, widget: toggle, label: Plate could not be read}
- id: recognition
label: Recognition
collapsed: true
fields:
- {field: ocrConfidence, widget: number, operator: gte, label: Minimum OCR confidence}
- {field: vehicleClass, widget: multiselect}
- {field: secondsVisible, widget: number, operator: gte, label: Visible for at least (seconds)}
{{ end -}}
{{- $config := include "hub.workflowConfiguration.settings" . | fromYaml -}}
{{- if and $config.enabled $config.includeDefaults (not (hasKey $config.replacements "workflow-contracts/anpr.yaml")) (or (eq .Values.mode "all") (eq .Values.mode "ui")) }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "hub.workflowConfiguration.anpr.configMapName" . }}
namespace: {{ .Release.Namespace }}
data:
anpr.yaml: |-
{{- include "hub.workflowConfiguration.anpr" . | nindent 4 }}
{{- end }}
@@ -0,0 +1,95 @@
{{/* Validate the rendered forwarder.yaml payload against schemas/workflow-contract.schema.json in the chart root. */}}
{{- define "hub.workflowConfiguration.forwarder" -}}
schemaVersion: 1
stage: forwarder
contractVersion: 1
presentation:
name: Forward to my system
description: Send the run's data to your own RabbitMQ destination.
icon: share
color: green
# Custom workflows supply their own connection; deployment credentials are never
# inherited. These non-secret defaults are stored on save, not read from the worker.
params:
- name: broker
label: Provider
description: Custom workflows use their own RabbitMQ destination. Shared deployment integrations are reserved for global workflows.
type: select
required: true
options: [rabbitmq]
optionLabels: {rabbitmq: RabbitMQ}
default: rabbitmq
group: Delivery
- name: mode
label: Completion mode
description: Complete after delivery, or wait for the external result. Callback requires a deployment-configured Hub callback URL.
type: select
required: true
options: [delivered, callback]
optionLabels: {delivered: Complete after delivery, callback: Wait for result}
default: delivered
group: Delivery
- name: host
label: Host
description: Your RabbitMQ destination host. Required before enabling the workflow.
type: string
required: true
placeholder: rabbitmq.example.com
group: RabbitMQ destination
- name: port
label: Port
description: Your RabbitMQ destination port, a whole number (usually 5671 with TLS or 5672 without TLS).
type: number
required: true
minimum: 1
maximum: 65535
group: RabbitMQ destination
- name: queue
label: Queue
description: Your destination queue. Its dead-letter queue is this name followed by .forwarder-dead-letter.
type: string
required: true
group: RabbitMQ destination
- name: vhost
label: Virtual host
description: Your RabbitMQ virtual host. Use / for the broker's default virtual host.
type: string
required: true
placeholder: /
group: RabbitMQ destination
- name: tls
label: Use TLS
description: Encrypt the connection and verify the broker certificate using system trust roots. Deployment TLS overrides are never inherited.
type: boolean
required: true
default: true
group: RabbitMQ destination
- name: username
label: Username
description: Your RabbitMQ destination username. Deployment credentials are never used.
type: string
required: true
group: RabbitMQ credentials
- name: password
label: Password
description: Your RabbitMQ destination password. Required before enabling the workflow; masked after saving.
type: secret
required: true
group: RabbitMQ credentials
# It contributes no output fields: connections after it can still use Start and
# earlier stages in Extra conditions.
fields: []
{{ end -}}
{{- $config := include "hub.workflowConfiguration.settings" . | fromYaml -}}
{{- if and $config.enabled $config.includeDefaults (not (hasKey $config.replacements "workflow-contracts/forwarder.yaml")) (or (eq .Values.mode "all") (eq .Values.mode "ui")) }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "hub.workflowConfiguration.forwarder.configMapName" . }}
namespace: {{ .Release.Namespace }}
data:
forwarder.yaml: |-
{{- include "hub.workflowConfiguration.forwarder" . | nindent 4 }}
{{- end }}
@@ -0,0 +1,75 @@
{{/* Validate the rendered start.yaml payload against schemas/workflow-contract.schema.json in the chart root. */}}
{{- define "hub.workflowConfiguration.start" -}}
schemaVersion: 1
stage: start
contractVersion: 1
# Paths refer to the safe initial context, not the full event or credentials.
fields:
- id: device
label: Device
path: device.deviceKey
type: string
operators: [eq, in]
choices:
source: project.devices
- id: sites
label: Sites
path: device.siteIds.*
type: string
operators: [in]
choices:
source: project.sites
- id: groups
label: Groups
path: device.groupIds.*
type: string
operators: [in]
choices:
source: project.groups
- id: classifications
label: Initial classifications
path: inputs.classify.details.*.classified
type: string
operators: [in]
optional: true
choices:
file: ../catalogs/classifications.json
# Condition-only context fields (not selectors): advanced conditions on
# Start connections may test only declared fields, under device. or inputs.
- id: deviceName
label: Camera name
path: device.deviceName
type: string
operators: [eq, in, contains, matches]
- id: objectClass
label: Detected object
path: inputs.classify.details.*.classified
type: string
operators: [eq, ne, in, exists]
optional: true
choices:
file: ../catalogs/classifications.json
# Controls offered for new authoring; an omitted control is not offered.
controls:
- id: weeklySchedule
label: Weekly schedule
- id: conditions
label: Advanced conditions
# Trigger modes offered for new authoring; omitted offers both.
triggerModes: [automatic, manual]
{{ end -}}
{{- include "hub.workflowConfiguration.validate" . -}}
{{- $config := include "hub.workflowConfiguration.settings" . | fromYaml -}}
{{- if and $config.enabled $config.includeDefaults (not (hasKey $config.replacements "workflow-contracts/start.yaml")) (or (eq .Values.mode "all") (eq .Values.mode "ui")) }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ printf "%s-workflow-configuration" .Release.Name | trunc 63 | trimSuffix "-" }}
namespace: {{ .Release.Namespace }}
data:
start.yaml: |-
{{- include "hub.workflowConfiguration.start" . | nindent 4 }}
{{- end }}
@@ -1,10 +0,0 @@
{{- if .Values.classificationCatalogEnabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: classification-catalog
namespace: {{ .Release.Namespace }}
data:
classifications.json: |-
{{- include "hub.classificationCatalog" . | nindent 4 }}
{{- end }}
@@ -1,12 +0,0 @@
{{- include "hub.workflowConfiguration.validate" . -}}
{{- $config := .Values.workflowConfiguration | default dict -}}
{{- if and $config.enabled (not $config.existingConfigMap) (or (eq .Values.mode "all") (eq .Values.mode "ui")) }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ printf "%s-workflow-configuration" .Release.Name | trunc 63 | trimSuffix "-" }}
namespace: {{ .Release.Namespace }}
data:
start.yaml: |-
{{- .Files.Get "configuration/workflow-contracts/start.yaml" | nindent 4 }}
{{- end }}
+81 -17
View File
@@ -1,5 +1,6 @@
{{- if or (eq .Values.mode "all") (eq .Values.mode "ui") -}}
{{- $workflowConfiguration := .Values.workflowConfiguration | default dict -}}
{{- $workflowConfiguration := include "hub.workflowConfiguration.settings" . | fromYaml -}}
{{- $catalogReplacement := index $workflowConfiguration.replacements "catalogs/classifications.json" | default dict -}}
{{- if .Values.kerberoshub.api.serviceEnabled }}
apiVersion: v1
kind: Service
@@ -120,12 +121,21 @@ spec:
template:
metadata:
annotations:
checksum/config-mongodb: {{ include (print $.Template.BasePath "/configmap-mongodb.yaml") . | sha256sum }}
{{- if .Values.classificationCatalogEnabled }}
checksum/classification-catalog: {{ include (print $.Template.BasePath "/configmap-classification-catalog.yaml") . | sha256sum }}
checksum/config-mongodb: {{ include (print $.Template.BasePath "/configMaps/configmap-mongodb.yaml") . | sha256sum }}
{{- if and .Values.classificationCatalogEnabled (not .Values.classificationCatalogExistingConfigMap) (not $catalogReplacement) }}
checksum/classification-catalog: {{ include (print $.Template.BasePath "/configMaps/catalogs/classifications.yaml") . | sha256sum }}
{{- end }}
{{- if and $workflowConfiguration.enabled (not $workflowConfiguration.existingConfigMap) }}
checksum/workflow-configuration: {{ include (print $.Template.BasePath "/configmap-workflow-configuration.yaml") . | sha256sum }}
{{- if include "kerberoshub.deviceTags.system.enabled" . }}
checksum/device-system-tags: {{ include (print $.Template.BasePath "/configMaps/catalogs/device-system-tags.yaml") . | sha256sum }}
{{- end }}
{{- if and $workflowConfiguration.enabled $workflowConfiguration.includeDefaults (not (hasKey $workflowConfiguration.replacements "workflow-contracts/start.yaml")) }}
checksum/workflow-configuration: {{ include (print $.Template.BasePath "/configMaps/workflow-contracts/start.yaml") . | sha256sum }}
{{- end }}
{{- if and $workflowConfiguration.enabled $workflowConfiguration.includeDefaults (not (hasKey $workflowConfiguration.replacements "workflow-contracts/anpr.yaml")) }}
checksum/workflow-contract-anpr: {{ include (print $.Template.BasePath "/configMaps/workflow-contracts/anpr.yaml") . | sha256sum }}
{{- end }}
{{- if and $workflowConfiguration.enabled $workflowConfiguration.includeDefaults (not (hasKey $workflowConfiguration.replacements "workflow-contracts/forwarder.yaml")) }}
checksum/workflow-contract-forwarder: {{ include (print $.Template.BasePath "/configMaps/workflow-contracts/forwarder.yaml") . | sha256sum }}
{{- end }}
labels:
app: hub-api
@@ -140,34 +150,64 @@ spec:
{{- $serverTLS := .Values.kerberoshub.api.serverTLS }}
{{- $mongodbTLS := .Values.mongodb.tls }}
{{- $classificationCatalogEnabled := .Values.classificationCatalogEnabled }}
{{- if or $workflowConfiguration.enabled $classificationCatalogEnabled .Values.kerberoshub.api.volumes (and $serverTLS.enabled $serverTLS.secretName) (and $mongodbTLS.enabled $mongodbTLS.existingSecret) }}
{{- $deviceSystemTagsEnabled := include "kerberoshub.deviceTags.system.enabled" . }}
{{- if or $workflowConfiguration.enabled $classificationCatalogEnabled $deviceSystemTagsEnabled .Values.kerberoshub.api.volumes (and $serverTLS.enabled $serverTLS.secretName) (and $mongodbTLS.enabled $mongodbTLS.existingSecret) }}
volumes:
{{- if $workflowConfiguration.enabled }}
- name: workflow-configuration
{{- if $workflowConfiguration.existingConfigMap }}
configMap:
name: {{ $workflowConfiguration.existingConfigMap | quote }}
items:
{{- toYaml $workflowConfiguration.items | nindent 14 }}
{{- else }}
projected:
sources:
{{- if $workflowConfiguration.includeDefaults }}
{{- if not (hasKey $workflowConfiguration.replacements "workflow-contracts/start.yaml") }}
- configMap:
name: {{ printf "%s-workflow-configuration" .Release.Name | trunc 63 | trimSuffix "-" }}
items:
- key: start.yaml
path: workflow-contracts/start.yaml
{{- end }}
{{- if not (hasKey $workflowConfiguration.replacements "workflow-contracts/anpr.yaml") }}
- configMap:
name: classification-catalog
name: {{ include "hub.workflowConfiguration.anpr.configMapName" . }}
items:
- key: anpr.yaml
path: workflow-contracts/anpr.yaml
{{- end }}
{{- if not (hasKey $workflowConfiguration.replacements "workflow-contracts/forwarder.yaml") }}
- configMap:
name: {{ include "hub.workflowConfiguration.forwarder.configMapName" . }}
items:
- key: forwarder.yaml
path: workflow-contracts/forwarder.yaml
{{- end }}
{{- if not $catalogReplacement }}
- configMap:
name: {{ include "hub.classificationCatalog.configMapName" . }}
items:
- key: classifications.json
path: catalogs/classifications.json
{{- end }}
{{- end }}
{{- end }}
{{- range $workflowConfiguration.configMaps }}
- configMap:
name: {{ .name | quote }}
items:
{{- toYaml .items | nindent 20 }}
{{- end }}
{{- end }}
{{- if $classificationCatalogEnabled }}
- name: classification-catalog
configMap:
name: classification-catalog
name: {{ include "hub.classificationCatalog.configMapName" . }}
{{- if or .Values.classificationCatalogExistingConfigMap $catalogReplacement }}
items:
- key: {{ default "classifications.json" $catalogReplacement.key | quote }}
path: classifications.json
{{- end }}
{{- end }}
{{- if $deviceSystemTagsEnabled }}
- name: device-system-tags
configMap:
name: device-system-tags
{{- end }}
{{- with .Values.kerberoshub.api.volumes }}
{{- toYaml . | nindent 8 }}
@@ -196,7 +236,7 @@ spec:
ports:
- containerPort: 80
name: http
{{- if or $workflowConfiguration.enabled $classificationCatalogEnabled .Values.kerberoshub.api.volumeMounts (and $serverTLS.enabled $serverTLS.secretName) (and $mongodbTLS.enabled $mongodbTLS.existingSecret) }}
{{- if or $workflowConfiguration.enabled $classificationCatalogEnabled $deviceSystemTagsEnabled .Values.kerberoshub.api.volumeMounts (and $serverTLS.enabled $serverTLS.secretName) (and $mongodbTLS.enabled $mongodbTLS.existingSecret) }}
volumeMounts:
{{- if $workflowConfiguration.enabled }}
- name: workflow-configuration
@@ -208,6 +248,11 @@ spec:
mountPath: /etc/kerberos/classifications
readOnly: true
{{- end }}
{{- if $deviceSystemTagsEnabled }}
- name: device-system-tags
mountPath: /etc/kerberos/device-tags
readOnly: true
{{- end }}
{{- with .Values.kerberoshub.api.volumeMounts}}
{{- toYaml . | nindent 12 }}
{{- end }}
@@ -230,6 +275,12 @@ spec:
- name: CLASSIFICATION_CATALOG_FILE
value: /etc/kerberos/classifications/classifications.json
{{- end }}
{{- if $deviceSystemTagsEnabled }}
# System tag catalog (deviceTags.system.catalog): the only system
# tags owners can assign. Unset (disabled) disables system tags.
- name: DEVICE_SYSTEM_TAGS_FILE
value: /etc/kerberos/device-tags/system-tags.json
{{- end }}
- name: ENVIRONMENT
value: "{{ .Values.environment }}"
- name: READ_ONLY
@@ -259,6 +310,9 @@ spec:
{{- if and .Values.kerberoshub.frontend.demoEnabled .Values.kerberoshub.frontend.demoUrl }}
{{- $corsOrigins = append $corsOrigins (printf "%s://%s" .Values.kerberoshub.frontend.schema .Values.kerberoshub.frontend.demoUrl) }}
{{- end }}
{{- range .Values.kerberoshub.api.corsAllowedOrigins }}
{{- $corsOrigins = append $corsOrigins . }}
{{- end }}
- name: CORS_ALLOWED_ORIGINS
value: {{ join "," $corsOrigins | quote }}
{{ if .Values.isPrivate }}
@@ -382,6 +436,15 @@ spec:
- name: MFA_ISSUER
value: "{{ .Values.kerberoshub.api.mfaIssuer }}"
{{- if .Values.kerberoshub.api.passkeys.enabled }}
- name: PASSKEY_RP_ID
value: {{ required "kerberoshub.api.passkeys.rpId is required when passkeys are enabled" .Values.kerberoshub.api.passkeys.rpId | quote }}
- name: PASSKEY_ORIGINS
value: {{ required "kerberoshub.api.passkeys.origins is required when passkeys are enabled" .Values.kerberoshub.api.passkeys.origins | join "," | quote }}
- name: PASSKEY_RP_NAME
value: {{ .Values.kerberoshub.api.passkeys.rpName | quote }}
{{- end }}
# AWS (Legacy, use Kerberos Vault instead)
- name: AWS_REGION
value: "{{ .Values.kerberoshub.api.aws.region }}"
@@ -543,5 +606,6 @@ spec:
value: "{{ .Values.opentelemetry.routingEnabled }}"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "{{ .Values.opentelemetry.collector.endpoint }}"
{{- include "hub.opentelemetry.apiSamplerEnv" $ | trim | nindent 12 }}
{{- end }}
{{- end }}
@@ -21,7 +21,7 @@ spec:
template:
metadata:
annotations:
checksum/config-mongodb: {{ include (print $.Template.BasePath "/configmap-mongodb.yaml") . | sha256sum }}
checksum/config-mongodb: {{ include (print $.Template.BasePath "/configMaps/configmap-mongodb.yaml") . | sha256sum }}
labels:
app: hub-audit
spec:
@@ -18,10 +18,14 @@ spec:
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap-mongodb.yaml") . | sha256sum }}
checksum/config: {{ include (print $.Template.BasePath "/configMaps/configmap-mongodb.yaml") . | sha256sum }}
labels:
app: hub-cleanup
spec:
{{- with .Values.kerberoshub.cleanup.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.kerberoshub.serviceAccount.create }}
serviceAccountName: {{ default (printf "%s-%s-sa" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-") .Values.kerberoshub.serviceAccount.name }}
{{- end }}
@@ -47,6 +51,10 @@ spec:
- name: hub-cleanup
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberoshub.cleanup.repository }}:{{ .Values.kerberoshub.cleanup.tag }}"
imagePullPolicy: {{ .Values.kerberoshub.cleanup.pullPolicy }}
{{- with .Values.kerberoshub.cleanup.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.kerberoshub.cleanup.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -111,6 +119,8 @@ spec:
value: "{{ .Values.kerberoshub.cleanup.defaultTaskRetentionDays }}"
- name: AUDIT_EVENT_RETENTION_DAYS
value: "{{ .Values.kerberoshub.cleanup.auditEventRetentionDays }}"
- name: WORKFLOW_RUN_RETENTION_DAYS
value: "{{ .Values.kerberoshub.cleanup.workflowRunRetentionDays | default "0" }}"
{{- if .Values.kerberoshub.extraEnv }}
{{- toYaml .Values.kerberoshub.extraEnv | nindent 12 }}
{{- end }}
@@ -10,7 +10,7 @@ spec:
ports:
- protocol: TCP
port: 80
targetPort: 80
targetPort: http
name: http
selector:
app: hub-frontend-demo
@@ -101,12 +101,16 @@ spec:
- name: hub-frontend-demo
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberoshub.frontend.repository }}:{{ .Values.kerberoshub.frontend.tag }}"
imagePullPolicy: {{ .Values.kerberoshub.frontend.pullPolicy }}
{{- with .Values.kerberoshub.frontend.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .Values.kerberoshub.frontend.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- containerPort: 80
- containerPort: {{ .Values.kerberoshub.frontend.containerPort | default 80 }}
name: http
{{- with .Values.kerberoshub.frontend.volumeMounts}}
volumeMounts:
@@ -192,6 +196,8 @@ spec:
# Google maps for using and displaying cameras, sites on a map
- name: GOOGLEMAPS_KEY
value: "{{ .Values.kerberoshub.frontend.googlemaps.apikey }}"
- name: GOOGLEMAPS_PLACES_API
value: "{{ .Values.kerberoshub.frontend.googlemaps.placesApi }}"
# features > chart
- name: CHART_COLORS
@@ -235,6 +241,8 @@ spec:
value: "{{ .Values.kerberoshub.frontend.features.workflows.enabled }}"
- name: FEATURE_AUDIT_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.audit.enabled }}"
- name: FEATURE_DAY_OVERVIEW_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.dayOverview.enabled }}"
- name: FEATURE_DEVICE_ANALYTICS_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.devices.analyticsEnabled }}"
- name: FEATURE_DASHBOARD_SITES_FULLSCREEN_ENABLED
@@ -11,7 +11,7 @@ spec:
ports:
- protocol: TCP
port: 80
targetPort: 80
targetPort: http
name: http
selector:
app: hub-frontend
@@ -182,12 +182,16 @@ spec:
- name: hub-frontend
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberoshub.frontend.repository }}:{{ .Values.kerberoshub.frontend.tag }}"
imagePullPolicy: {{ .Values.kerberoshub.frontend.pullPolicy }}
{{- with .Values.kerberoshub.frontend.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .Values.kerberoshub.frontend.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- containerPort: 80
- containerPort: {{ .Values.kerberoshub.frontend.containerPort | default 80 }}
name: http
{{- with .Values.kerberoshub.frontend.volumeMounts}}
volumeMounts:
@@ -275,6 +279,8 @@ spec:
# Google maps for using and displaying cameras, sites on a map
- name: GOOGLEMAPS_KEY
value: "{{ .Values.kerberoshub.frontend.googlemaps.apikey }}"
- name: GOOGLEMAPS_PLACES_API
value: "{{ .Values.kerberoshub.frontend.googlemaps.placesApi }}"
# Zendesk for support
- name: ZENDESK_URL
@@ -314,6 +320,8 @@ spec:
value: "{{ .Values.kerberoshub.frontend.features.workflows.enabled }}"
- name: FEATURE_AUDIT_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.audit.enabled }}"
- name: FEATURE_DAY_OVERVIEW_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.dayOverview.enabled }}"
- name: FEATURE_DEVICE_ANALYTICS_ENABLED
value: "{{ .Values.kerberoshub.frontend.features.devices.analyticsEnabled }}"
- name: FEATURE_DASHBOARD_SITES_FULLSCREEN_ENABLED
@@ -18,10 +18,14 @@ spec:
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap-mongodb.yaml") . | sha256sum }}
checksum/config: {{ include (print $.Template.BasePath "/configMaps/configmap-mongodb.yaml") . | sha256sum }}
labels:
app: hub-monitor-device
spec:
{{- with .Values.kerberoshub.monitordevice.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.kerberoshub.serviceAccount.create }}
serviceAccountName: {{ default (printf "%s-%s-sa" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-") .Values.kerberoshub.serviceAccount.name }}
{{- end }}
@@ -47,6 +51,10 @@ spec:
- name: hub-monitor-device
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberoshub.monitordevice.repository }}:{{ .Values.kerberoshub.monitordevice.tag }}"
imagePullPolicy: {{ .Values.kerberoshub.monitordevice.pullPolicy }}
{{- with .Values.kerberoshub.monitordevice.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.kerberoshub.monitordevice.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -67,6 +75,8 @@ spec:
{{- include "hub.mongodb.appUriEnv" (dict "root" $ "app" "hub-monitor-device") | nindent 12 }}
- name: LOG_LEVEL
value: "{{ .Values.kerberoshub.monitordevice.logLevel }}"
- name: DEVICE_SCAN_INTERVAL_SECONDS
value: "{{ .Values.kerberoshub.monitordevice.scanIntervalSeconds | default 60 }}"
- name: READ_ONLY
value: "{{ .Values.readonly }}"
# Mail settings
@@ -82,6 +92,13 @@ spec:
value: "{{ .Values.email.templates.device }}"
- name: DEVICE_TITLE
value: "{{ .Values.email.templates.deviceTitle }}"
- name: DEVICE_GROUP_TEMPLATE
value: "{{ .Values.email.templates.deviceGroup | default "device_group" }}"
- name: DEVICE_GROUP_TITLE
value: "{{ .Values.email.templates.deviceGroupTitle | default "[Device] Several cameras changed status" }}"
# Base URL for the link in grouped device notifications.
- name: PUBLIC_URL
value: "{{ .Values.kerberoshub.frontend.schema }}://{{ .Values.kerberoshub.frontend.url }}"
# - Plain SMTP
- name: SMTP_SERVER
@@ -18,7 +18,7 @@ spec:
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap-mongodb.yaml") . | sha256sum }}
checksum/config: {{ include (print $.Template.BasePath "/configMaps/configmap-mongodb.yaml") . | sha256sum }}
labels:
app: hub-reactivate-subscription
spec:
@@ -21,6 +21,64 @@ workflow later cannot silently alias another workflow's runs or references.
{{- end -}}
{{- $_ := set $identities $id $name -}}
{{- end -}}
{{- include "kerberoshub.workflows.validateSystemTags" . -}}
{{- end -}}
{{/*
Guard the definitions that can select devices by system tag. The engine reads
JSON keys case-insensitively and ignores unknown ones, so a misspelled or
re-cased key silently changes what a definition selects (a dropped
systemTags scope makes a global workflow run on every device). Therefore:
- definitions and triggers may only use the keys the engine knows, spelled
exactly (definition: id, enabled, triggers, stages; trigger: the
models.WorkflowTrigger fields);
- while deviceTags.system.enabled is false, no definition may mention system
tags anywhere (trigger systemTags, or a device.systemTags condition at any
depth, in triggers, shared conditions or stage needs), matched
case-insensitively on the whole definition;
- every system tag a trigger selects must be in deviceTags.system.catalog,
so a typo fails rendering instead of never matching a device.
Disabled definitions are checked too, so enabling one cannot fail later.
*/}}
{{- define "kerberoshub.workflows.validateSystemTags" -}}
{{- include "kerberoshub.deviceTags.system.validate" . -}}
{{- $catalog := dict -}}
{{- range $tag := (include "kerberoshub.deviceTags.system.catalog" . | fromJsonArray) -}}
{{- $_ := set $catalog (toString $tag.key) true -}}
{{- end -}}
{{- $enabled := include "kerberoshub.deviceTags.system.enabled" . -}}
{{- $definitionKeys := list "id" "enabled" "triggers" "stages" -}}
{{- $triggerKeys := list "type" "edgeId" "sharedConditions" "devices" "siteIds" "groupIds" "systemTags" "classifications" "conditionMode" "conditions" "weeklySchedule" "surfaces" -}}
{{- range $name, $wf := .Values.kerberoshub.workflows.definitions -}}
{{- if $wf -}}
{{- if not (kindIs "map" $wf) -}}
{{- fail (printf "kerberoshub.workflows.definitions[%q] must be an object" $name) -}}
{{- end -}}
{{- if and (not $enabled) (contains "systemtags" (lower (toJson $wf))) -}}
{{- fail (printf "kerberoshub.workflows.definitions[%q] selects devices by system tag (systemTags or a device.systemTags condition), but deviceTags.system.enabled is false" $name) -}}
{{- end -}}
{{- range $key, $_ := $wf -}}
{{- if not (has $key $definitionKeys) -}}
{{- fail (printf "kerberoshub.workflows.definitions[%q]: unknown key %q (allowed: %s). Keys are case-sensitive; a misspelled key would be ignored" $name $key (join ", " $definitionKeys)) -}}
{{- end -}}
{{- end -}}
{{- range $i, $trigger := ($wf.triggers | default list) -}}
{{- if not (kindIs "map" $trigger) -}}
{{- fail (printf "kerberoshub.workflows.definitions[%q].triggers[%d] must be an object" $name $i) -}}
{{- end -}}
{{- range $key, $_ := $trigger -}}
{{- if not (has $key $triggerKeys) -}}
{{- fail (printf "kerberoshub.workflows.definitions[%q].triggers[%d]: unknown key %q (allowed: %s). Keys are case-sensitive; a misspelled key would be ignored and widen the trigger" $name $i $key (join ", " $triggerKeys)) -}}
{{- end -}}
{{- end -}}
{{- range $tag := ($trigger.systemTags | default list) -}}
{{- if not (hasKey $catalog (toString $tag)) -}}
{{- fail (printf "kerberoshub.workflows.definitions[%q].triggers[%d].systemTags: %q is not in deviceTags.system.catalog; add it to the system tag catalog" $name $i (toString $tag)) -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
@@ -50,6 +50,10 @@ spec:
labels:
app: hub-{{ $name }}
spec:
{{- with $svc.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if $root.Values.kerberoshub.serviceAccount.create }}
serviceAccountName: {{ default (printf "%s-%s-sa" $root.Release.Name $root.Chart.Name | trunc 63 | trimSuffix "-") $root.Values.kerberoshub.serviceAccount.name }}
{{- end }}
@@ -69,6 +73,10 @@ spec:
- name: hub-{{ $name }}
image: "{{ $root.Values.global.imageRegistry }}{{ $svc.repository }}:{{ $svc.tag }}"
imagePullPolicy: {{ $svc.pullPolicy | default "IfNotPresent" }}
{{- with $svc.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with $svc.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -117,6 +125,14 @@ spec:
- name: KERBEROS_STORAGE_PROVIDER
value: {{ . | quote }}
{{- end }}
{{- if $root.Values.opentelemetry.enabled }}
# OpenTelemetry tracing: the stage continues the engine's dispatch span
# (the run's traceparent) and follows its sampling decision.
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "{{ $root.Values.opentelemetry.collector.endpoint }}"
{{- include "hub.opentelemetry.samplerEnv" $root | trim | nindent 8 }}
{{- end }}
# Per-stage tuning knobs. Any key/value under services.<name>.env is
# rendered verbatim as container env, so a worker can be tuned from
@@ -19,10 +19,14 @@ spec:
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap-mongodb.yaml") . | sha256sum }}
checksum/config: {{ include (print $.Template.BasePath "/configMaps/configmap-mongodb.yaml") . | sha256sum }}
labels:
app: hub-workflows
spec:
{{- with .Values.kerberoshub.services.workflows.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.kerberoshub.serviceAccount.create }}
serviceAccountName: {{ default (printf "%s-%s-sa" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-") .Values.kerberoshub.serviceAccount.name }}
{{- end }}
@@ -48,6 +52,10 @@ spec:
- name: hub-workflows
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberoshub.services.workflows.repository }}:{{ .Values.kerberoshub.services.workflows.tag }}"
imagePullPolicy: {{ .Values.kerberoshub.services.workflows.pullPolicy }}
{{- with .Values.kerberoshub.services.workflows.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .Values.kerberoshub.services.workflows.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -115,6 +123,7 @@ spec:
value: "{{ .Values.opentelemetry.routingEnabled }}"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "{{ .Values.opentelemetry.collector.endpoint }}"
{{- include "hub.opentelemetry.samplerEnv" $ | trim | nindent 8 }}
{{- end }}
---
apiVersion: v1
@@ -22,7 +22,7 @@ spec:
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap-mongodb.yaml") . | sha256sum }}
checksum/config: {{ include (print $.Template.BasePath "/configMaps/configmap-mongodb.yaml") . | sha256sum }}
labels:
app: pipe-analysis
spec:
@@ -121,6 +121,20 @@ spec:
# Sprites
- name: SPRITE_ENABLED
value: "{{ .Values.kerberospipeline.sprite.enabled }}"
# Dominant colors
- name: DOMINANTCOLOR_ENABLED
value: "{{ .Values.kerberospipeline.dominantColor.enabled }}"
# Thumbnail regeneration from classified frames (opt-in)
- name: THUMBNAIL_REGENERATION_ENABLED
value: {{ .Values.kerberospipeline.analysis.thumbnailRegeneration.enabled | default false | quote }}
# In-memory cache for hot-path MongoDB lookups
- name: DB_CACHE_ENABLED
value: {{ .Values.kerberospipeline.analysis.dbCache.enabled | default false | quote }}
- name: DB_CACHE_TTL
value: {{ .Values.kerberospipeline.analysis.dbCache.ttl | default "5m" | quote }}
{{- if .Values.opentelemetry.enabled }}
# Open Telemetry tracing
@@ -130,6 +144,7 @@ spec:
value: "{{ .Values.opentelemetry.routingEnabled }}"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "{{ .Values.opentelemetry.collector.endpoint }}"
{{- include "hub.opentelemetry.samplerEnv" $ | trim | nindent 8 }}
{{- end }}
---
apiVersion: v1
@@ -24,6 +24,10 @@ spec:
labels:
app: pipe-counting
spec:
{{- with .Values.kerberospipeline.counting.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.kerberoshub.serviceAccount.create }}
serviceAccountName: {{ default (printf "%s-%s-sa" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-") .Values.kerberoshub.serviceAccount.name }}
{{- end }}
@@ -43,6 +47,10 @@ spec:
- name: pipe-counting
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberospipeline.counting.repository }}:{{ .Values.kerberospipeline.counting.tag }}"
imagePullPolicy: {{ .Values.kerberospipeline.counting.pullPolicy }}
{{- with .Values.kerberospipeline.counting.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .Values.kerberospipeline.counting.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -89,6 +97,7 @@ spec:
value: "{{ .Values.opentelemetry.routingEnabled }}"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "{{ .Values.opentelemetry.collector.endpoint }}"
{{- include "hub.opentelemetry.samplerEnv" $ | trim | nindent 8 }}
{{- end }}
---
apiVersion: v1
@@ -1,4 +1,4 @@
{{- if or (eq .Values.mode "all") (eq .Values.mode "pipeline") -}}
{{- if and (or (eq .Values.mode "all") (eq .Values.mode "pipeline")) (eq .Values.kerberospipeline.dominantColor.enabled true) -}}
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -24,6 +24,10 @@ spec:
labels:
app: pipe-dominantcolor
spec:
{{- with .Values.kerberospipeline.dominantColor.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.kerberoshub.serviceAccount.create }}
serviceAccountName: {{ default (printf "%s-%s-sa" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-") .Values.kerberoshub.serviceAccount.name }}
{{- end }}
@@ -43,6 +47,10 @@ spec:
- name: pipe-dominantcolor
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberospipeline.dominantColor.repository }}:{{ .Values.kerberospipeline.dominantColor.tag }}"
imagePullPolicy: {{ .Values.kerberospipeline.dominantColor.pullPolicy }}
{{- with .Values.kerberospipeline.dominantColor.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .Values.kerberospipeline.dominantColor.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -89,6 +97,7 @@ spec:
value: "{{ .Values.opentelemetry.routingEnabled }}"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "{{ .Values.opentelemetry.collector.endpoint }}"
{{- include "hub.opentelemetry.samplerEnv" $ | trim | nindent 8 }}
{{- end }}
---
apiVersion: v1
@@ -22,7 +22,7 @@ spec:
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap-mongodb.yaml") . | sha256sum }}
checksum/config: {{ include (print $.Template.BasePath "/configMaps/configmap-mongodb.yaml") . | sha256sum }}
labels:
app: pipe-event
spec:
@@ -110,6 +110,7 @@ spec:
value: "{{ .Values.opentelemetry.routingEnabled }}"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "{{ .Values.opentelemetry.collector.endpoint }}"
{{- include "hub.opentelemetry.samplerEnv" $ | trim | nindent 8 }}
{{- end }}
---
apiVersion: v1
@@ -18,7 +18,7 @@ spec:
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap-mongodb.yaml") . | sha256sum }}
checksum/config: {{ include (print $.Template.BasePath "/configMaps/configmap-mongodb.yaml") . | sha256sum }}
labels:
app: pipe-export
spec:
@@ -142,6 +142,7 @@ spec:
value: "{{ .Values.opentelemetry.routingEnabled }}"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "{{ .Values.opentelemetry.collector.endpoint }}"
{{- include "hub.opentelemetry.samplerEnv" $ | trim | nindent 8 }}
{{- end }}
---
apiVersion: v1
@@ -22,7 +22,7 @@ spec:
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap-mongodb.yaml") . | sha256sum }}
checksum/config: {{ include (print $.Template.BasePath "/configMaps/configmap-mongodb.yaml") . | sha256sum }}
labels:
app: pipe-monitor
spec:
@@ -131,6 +131,7 @@ spec:
value: "{{ .Values.opentelemetry.routingEnabled }}"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "{{ .Values.opentelemetry.collector.endpoint }}"
{{- include "hub.opentelemetry.samplerEnv" $ | trim | nindent 8 }}
{{- end }}
---
apiVersion: v1
@@ -18,7 +18,7 @@ spec:
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap-mongodb.yaml") . | sha256sum }}
checksum/config: {{ include (print $.Template.BasePath "/configMaps/configmap-mongodb.yaml") . | sha256sum }}
labels:
app: pipe-notify-test
spec:
@@ -130,6 +130,7 @@ spec:
value: "{{ .Values.opentelemetry.routingEnabled }}"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "{{ .Values.opentelemetry.collector.endpoint }}"
{{- include "hub.opentelemetry.samplerEnv" $ | trim | nindent 8 }}
{{- end }}
---
apiVersion: v1
@@ -22,10 +22,14 @@ spec:
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap-mongodb.yaml") . | sha256sum }}
checksum/config: {{ include (print $.Template.BasePath "/configMaps/configmap-mongodb.yaml") . | sha256sum }}
labels:
app: pipe-notify
spec:
{{- with .Values.kerberospipeline.notify.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.kerberoshub.serviceAccount.create }}
serviceAccountName: {{ default (printf "%s-%s-sa" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-") .Values.kerberoshub.serviceAccount.name }}
{{- end }}
@@ -51,6 +55,10 @@ spec:
- name: pipe-notify
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberospipeline.notify.repository }}:{{ .Values.kerberospipeline.notify.tag }}"
imagePullPolicy: {{ .Values.kerberospipeline.notify.pullPolicy }}
{{- with .Values.kerberospipeline.notify.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .Values.kerberospipeline.notify.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -176,6 +184,7 @@ spec:
value: "{{ .Values.opentelemetry.routingEnabled }}"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "{{ .Values.opentelemetry.collector.endpoint }}"
{{- include "hub.opentelemetry.samplerEnv" $ | trim | nindent 8 }}
{{- end }}
---
apiVersion: v1
@@ -22,7 +22,7 @@ spec:
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap-mongodb.yaml") . | sha256sum }}
checksum/config: {{ include (print $.Template.BasePath "/configMaps/configmap-mongodb.yaml") . | sha256sum }}
labels:
app: pipe-sequence
spec:
@@ -69,6 +69,8 @@ spec:
name: mongodb-config
env:
{{- include "hub.mongodb.appUriEnv" (dict "root" $ "app" "hub-pipeline-sequence") | nindent 8 }}
- name: LOG_LEVEL
value: "{{ .Values.kerberospipeline.sequence.logLevel }}"
- name: CLOUD_PROVIDER
value: "{{ .Values.cloudProvider }}"
- name: QUEUE_SYSTEM
@@ -104,6 +106,7 @@ spec:
value: "{{ .Values.opentelemetry.routingEnabled }}"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "{{ .Values.opentelemetry.collector.endpoint }}"
{{- include "hub.opentelemetry.samplerEnv" $ | trim | nindent 8 }}
{{- end }}
---
apiVersion: v1
@@ -87,6 +87,10 @@ spec:
value: "{{ .Values.rabbitmq.username }}"
- name: RABBITMQ_PASSWORD
value: "{{ .Values.rabbitmq.password }}"
{{- with .Values.kerberospipeline.sprite.prefetchCount }}
- name: RABBITMQ_PREFETCH_COUNT
value: {{ . | quote }}
{{- end }}
# There is an option to store the sprites in a storage provider in Kerberos Vault.
- name: VAULT_SPRITE_ENABLED
@@ -112,6 +116,7 @@ spec:
value: "{{ .Values.opentelemetry.routingEnabled }}"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "{{ .Values.opentelemetry.collector.endpoint }}"
{{- include "hub.opentelemetry.samplerEnv" $ | trim | nindent 8 }}
{{- end }}
---
apiVersion: v1
@@ -22,10 +22,14 @@ spec:
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap-mongodb.yaml") . | sha256sum }}
checksum/config: {{ include (print $.Template.BasePath "/configMaps/configmap-mongodb.yaml") . | sha256sum }}
labels:
app: pipe-throttler
spec:
{{- with .Values.kerberospipeline.throttler.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.kerberoshub.serviceAccount.create }}
serviceAccountName: {{ default (printf "%s-%s-sa" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-") .Values.kerberoshub.serviceAccount.name }}
{{- end }}
@@ -51,6 +55,10 @@ spec:
- name: pipe-throttler
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberospipeline.throttler.repository }}:{{ .Values.kerberospipeline.throttler.tag }}"
imagePullPolicy: {{ .Values.kerberospipeline.throttler.pullPolicy }}
{{- with .Values.kerberospipeline.throttler.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .Values.kerberospipeline.throttler.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -106,6 +114,7 @@ spec:
value: "{{ .Values.opentelemetry.routingEnabled }}"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "{{ .Values.opentelemetry.collector.endpoint }}"
{{- include "hub.opentelemetry.samplerEnv" $ | trim | nindent 8 }}
{{- end }}
---
apiVersion: v1
@@ -24,6 +24,10 @@ spec:
labels:
app: pipe-thumbnail
spec:
{{- with .Values.kerberospipeline.thumbnail.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.kerberoshub.serviceAccount.create }}
serviceAccountName: {{ default (printf "%s-%s-sa" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-") .Values.kerberoshub.serviceAccount.name }}
{{- end }}
@@ -43,6 +47,10 @@ spec:
- name: pipe-thumbnail
image: "{{ .Values.global.imageRegistry }}{{ .Values.kerberospipeline.thumbnail.repository }}:{{ .Values.kerberospipeline.thumbnail.tag }}"
imagePullPolicy: {{ .Values.kerberospipeline.thumbnail.pullPolicy }}
{{- with .Values.kerberospipeline.thumbnail.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .Values.kerberospipeline.thumbnail.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -87,6 +95,10 @@ spec:
value: "{{ .Values.rabbitmq.username }}"
- name: RABBITMQ_PASSWORD
value: "{{ .Values.rabbitmq.password }}"
{{- with .Values.kerberospipeline.thumbnail.prefetchCount }}
- name: RABBITMQ_PREFETCH_COUNT
value: {{ . | quote }}
{{- end }}
# Thumbnail generation
- name: THUMBNAIL_QUALITY
@@ -95,6 +107,8 @@ spec:
value: "{{ .Values.kerberospipeline.thumbnail.width }}"
- name: THUMBNAIL_HEIGHT
value: "{{ .Values.kerberospipeline.thumbnail.height }}"
- name: THUMBNAIL_SKIP_NON_KEYFRAMES
value: "{{ .Values.kerberospipeline.thumbnail.skipNonKeyframes }}"
# There is an option to store the thumbnails in a storage provider in Kerberos Vault.
# This will lower storage in mongodb.
@@ -115,6 +129,7 @@ spec:
value: "{{ .Values.opentelemetry.routingEnabled }}"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "{{ .Values.opentelemetry.collector.endpoint }}"
{{- include "hub.opentelemetry.samplerEnv" $ | trim | nindent 8 }}
{{- end }}
---
@@ -19,7 +19,7 @@ spec:
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap-mongodb.yaml") . | sha256sum }}
checksum/config: {{ include (print $.Template.BasePath "/configMaps/configmap-mongodb.yaml") . | sha256sum }}
labels:
app: vault-forwarder
spec:
+157 -11
View File
@@ -2,13 +2,135 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"classificationCatalog": {"type": ["array", "null"]},
"classificationCatalogExistingConfigMap": {
"type": "string",
"maxLength": 253,
"pattern": "^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$"
},
"classificationCatalog": {
"type": ["array", "null"],
"minItems": 1,
"description": "Optional inline classification catalog in the Hub shared choice catalog format; replaces the bundled defaults in the chart's classification-catalog ConfigMap. Omit (or null) for the bundled defaults. Keys must be unique (checked when rendering).",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["key", "label"],
"properties": {
"key": {"type": "string", "minLength": 1},
"label": {"type": "string", "minLength": 1},
"icon": {"type": "string", "minLength": 1}
}
}
},
"deviceTags": {
"type": "object",
"additionalProperties": false,
"properties": {
"system": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"description": "Owners may assign system tags and global workflows may select devices by them. Only for deployments whose owners are trusted (self-hosted)."
},
"catalog": {
"type": "array",
"description": "The system tags owners can assign, in the Hub shared choice catalog format (key, label) plus an optional description. Keys must be unique (checked when rendering).",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["key", "label"],
"properties": {
"key": {
"type": "string",
"maxLength": 64,
"pattern": "^[a-z0-9]([a-z0-9._:-]*[a-z0-9])?$"
},
"label": {"type": "string", "minLength": 1},
"description": {"type": "string"}
}
}
}
}
}
}
},
"kerberoshub": {
"type": "object",
"properties": {
"api": {
"type": "object",
"properties": {
"passkeys": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {"type": "boolean"},
"rpId": {
"type": "string",
"description": "Stable WebAuthn relying-party hostname, without scheme, port or path. Must match each frontend origin's hostname or be its parent domain."
},
"origins": {
"type": "array",
"uniqueItems": true,
"items": {
"type": "string",
"pattern": "^https://[^/?#]+$|^http://localhost(:[0-9]+)?$"
},
"description": "Exact trusted frontend origins, with no path or trailing slash. HTTPS is required except for HTTP localhost development."
},
"rpName": {"type": "string"}
},
"if": {
"required": ["enabled"],
"properties": {"enabled": {"const": true}}
},
"then": {
"required": ["rpId", "origins"],
"properties": {
"rpId": {"minLength": 1},
"origins": {"minItems": 1}
}
}
},
"corsAllowedOrigins": {
"type": "array",
"items": {
"type": "string",
"pattern": "^https?://[^/?#]+$"
}
}
}
}
}
},
"workflowConfiguration": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "existingConfigMap", "items"],
"required": ["enabled"],
"not": {
"allOf": [
{"anyOf": [{"required": ["includeDefaults"]}, {"required": ["configMaps"]}]},
{"anyOf": [{"required": ["existingConfigMap"]}, {"required": ["items"]}, {"required": ["extraConfigMaps"]}]}
]
},
"properties": {
"enabled": {"type": "boolean"},
"includeDefaults": {
"type": "boolean",
"description": "Include bundled files not explicitly replaced by configMaps items; true when omitted."
},
"configMaps": {
"type": "array",
"items": {"$ref": "#/definitions/configMapSource"},
"description": "Add files or explicitly replace a bundled file using replace: true on an item."
},
"extraConfigMaps": {
"type": "array",
"description": "Legacy additive sources; use configMaps for new deployments.",
"items": {"$ref": "#/definitions/configMapSource"}
},
"existingConfigMap": {
"type": "string",
"maxLength": 253,
@@ -16,15 +138,39 @@
},
"items": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["key", "path"],
"properties": {
"key": {"type": "string", "pattern": "^[A-Za-z0-9._-]+$"},
"path": {"type": "string", "pattern": "^(workflow-contracts|catalogs)/[A-Za-z0-9_-][A-Za-z0-9._-]*\\.(yaml|json)$"}
}
}
"items": {"$ref": "#/definitions/configMapItem"}
}
}
}
},
"definitions": {
"configMapSource": {
"type": "object",
"additionalProperties": false,
"required": ["name", "items"],
"properties": {
"name": {
"type": "string",
"maxLength": 253,
"pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$"
},
"items": {
"type": "array",
"minItems": 1,
"items": {"$ref": "#/definitions/configMapItem"}
}
}
},
"configMapItem": {
"type": "object",
"additionalProperties": false,
"required": ["key", "path"],
"properties": {
"key": {"type": "string", "pattern": "^[A-Za-z0-9._-]+$"},
"path": {"type": "string", "pattern": "^(workflow-contracts|catalogs)/[A-Za-z0-9_-][A-Za-z0-9._-]*\\.(yaml|json)$"},
"replace": {
"type": "boolean",
"description": "Replace this whole bundled file, not individual fields/entries. Requires configMaps and includeDefaults: true; the path must identify a bundled file."
}
}
}
+152 -18
View File
@@ -78,20 +78,60 @@ mongodb:
# downstream processing. Classifier output keys must match these values.
# Disable this to use the legacy classification catalog stored in MongoDB.
classificationCatalogEnabled: true
# classificationCatalog is optional and omitted by default, which uses
# configuration/catalogs/classifications.json. Set it to a list to override.
# Explicit null also uses defaults; [] retains the API fallback behavior.
# Optional existing ConfigMap in the release namespace with classifications.json.
# When set, do not create a catalog ConfigMap; this replaces bundled defaults.
classificationCatalogExistingConfigMap: ""
# Defaults live in templates/configMaps/catalogs/classifications.yaml.
# Optionally define the complete catalog inline instead; it replaces the
# defaults in the chart's classification-catalog ConfigMap (and so the bundled
# catalogs/classifications.json). Entries are {key, label, icon?} with unique
# keys. Do not combine it with classificationCatalogExistingConfigMap or a
# catalogs/classifications.json replacement in workflowConfiguration.
# classificationCatalog:
# - key: car
# label: Car
# icon: car
# Preparatory configuration packaging only: the API does not yet load workflow
# contracts. Enabling this mounts files but does not activate a new editor/runtime.
# Device tags, grouped by type. System tags are defined here and assigned by
# owners (a multi-select on the device page); global workflows select devices
# across organisations by system tag (`systemTags` in a trigger). Owners set
# them, so any owner could enroll devices into such a workflow: enable only
# where owners are the deployment's administrators (self-hosted), never on
# multi-tenant SaaS. While disabled, hub-api gets an empty catalog (system tags
# cannot be set) and rendering fails on workflows that select devices by
# system tag.
deviceTags:
system:
enabled: false # a boolean (true/false), not a string
# The only system tags owners can assign, in the Hub shared choice catalog
# format (as catalogs/classifications.json) plus an optional description.
# Rendered into the device-system-tags ConfigMap for hub-api.
# key stored tag: 1-64 characters of a-z, 0-9, ".", "_", ":" or
# "-", starting and ending with a letter or digit; unique.
# label display name.
# description optional help text shown with the tag.
# Workflow trigger systemTags must exist here; otherwise rendering fails.
catalog: []
# - key: anpr-pilot
# label: ANPR pilot
# description: Runs licence plate recognition on this camera
# Configuration packaging only: consuming contracts requires a compatible
# API/frontend. Enabling this mounts files but does not register stage workers.
workflowConfiguration:
enabled: false
# Empty uses the bundled Start contract and effective classification catalog.
# Otherwise mount this existing ConfigMap instead; it must be in this namespace.
existingConfigMap: ""
# Required only with existingConfigMap: map ConfigMap keys to relative files
# under /etc/kerberos/configuration (workflow-contracts/... or catalogs/...).
items: []
# Omitted includeDefaults means true; false requires a nonempty configMaps list.
# includeDefaults: true
# configMaps: []
# Each ConfigMap supplies name and items [{key, path, replace?}] in the release namespace.
# An item with replace: true replaces one bundled file while retaining other defaults.
# Replacement requires includeDefaults: true and a known bundled destination path.
# Unmarked collisions and repeated custom destinations are rejected.
# Replacing catalogs/classifications.json also supplies the classification API:
# do not combine it with classificationCatalogExistingConfigMap or classificationCatalog.
# Legacy existingConfigMap/items/extraConfigMaps remain supported, but cannot
# be mixed with includeDefaults/configMaps. Both interfaces are omitted here
# so Helm's default merging does not introduce a false mixed configuration.
###################################################
# MQTT configuration (bi-directional communication)
###################################################
@@ -169,6 +209,27 @@ opentelemetry:
# the collector terminates TLS. A scheme-less value defaults to TLS and
# fails against a plaintext collector ("server gave HTTP response to HTTPS client").
endpoint: "http://otel-collector:4318"
# Trace sampling is decided where a trace starts:
# - Uploads start at the Vault, whose deployment sets their rate
# (TRACE_SAMPLER_RATIO_UPLOAD / TRACE_SAMPLER_RATIO_OTHER).
# - hub-api starts its own traces; apiSamplingRatios below sets its rates.
# hub-workflows, the pipe-* services and the workflow stages only continue
# traces started elsewhere: they follow that decision and never start traces
# of their own (OTEL_TRACES_SAMPLER=parentbased_always_off, not configurable).
#
# apiSamplingRatios: hub-api's sampling ratio (0 to 1) per kind of work,
# rendered as TRACE_SAMPLER_RATIO_<CLASS> (hub-api on trace library v1.2.6+).
# Empty values are not rendered; with none set, hub-api records every trace.
# operator: requests from users and API clients (UI, API tokens).
# other: everything else hub-api starts: device heartbeats, agent uploads
# and live segments, background work, queries outside a request.
# Unset with operator set: records everything.
# Example: ~10% of user requests, 1% of everything else:
# operator: "0.1"
# other: "0.01"
apiSamplingRatios:
operator: ""
other: ""
############################################
# OpenAI configuration (semantic search)
#
@@ -207,6 +268,11 @@ kerberosvault:
###########################################################################
admin:
repository: uugai/admin
# Opt in to 8080 and a securityContext only with a compatible hardened image.
containerPort: 80
securityContext: {}
readinessProbe: {}
livenessProbe: {}
pullPolicy: IfNotPresent
tag: "v1.3.0"
replicas: 2
@@ -285,6 +351,9 @@ kerberoshub:
jwtSecret: "this-is-a-secret-please-change-to-random-string" # change to a random value, this is for generating JWT tokens.
schema: "https"
url: "api.yourdomain.com"
# Additional complete HTTP(S) origins allowed to call the Hub API.
# The configured frontend origins remain allowed automatically.
corsAllowedOrigins: []
# Temporarily stop serving the polled GET /devices list: hub-api answers 204
# without querying MongoDB. The frontend still loads devices once through
# /session/bootstrap, but device online/offline status stops refreshing.
@@ -328,6 +397,13 @@ kerberoshub:
# MFA issuer name
mfaIssuer: "yourdomain.com"
# WebAuthn passkeys for users and sub-users; requires compatible API/frontend images.
# Use the public frontend hostname/origins, not the API URL. Keep rpId stable.
passkeys:
enabled: false
rpId: "" # e.g. "hub.example.com", without scheme, port or path.
origins: [] # e.g. ["https://hub.example.com"]; exact origins, no trailing slash.
rpName: "Kerberos Hub"
# Admin API's are made available for automation of Kerberos Hub.
# To access those API's (e.g. creation of owner users), an API key needs to be provided.
apiKey: "a-random-admin-api-key"
@@ -403,6 +479,9 @@ kerberoshub:
pullPolicy: IfNotPresent
tag: "v1.13.9"
replicas: 2
# Hardened frontend images listen on 8080; retain 80 for existing images.
containerPort: 80
securityContext: {}
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
# Set to false to skip rendering the hub-frontend Service (e.g. when an
# external/shared Service is managed elsewhere).
@@ -528,6 +607,9 @@ kerberoshub:
# Audit events are visible to owner/admin users when enabled.
audit:
enabled: "true" # Enable or disable the Audit events page and navigation link 'true' or 'false'
# Day overview page and its navigation menu; role page permissions still apply.
dayOverview:
enabled: "true" # Enable or disable the Day overview page and navigation menu 'true' or 'false'
# Organisation controls remain visible as a read-only current organisation when switching is disabled.
organisations:
enabled: "" # Enable or disable the organisation feature family, including projects; when empty, child groups apply independently
@@ -652,6 +734,7 @@ kerberoshub:
publicKey: ""
googlemaps: # Google maps is used inside the application to visualise cameras and sites.
apikey: "xxxx"
placesApi: "legacy" # Places API for address autocomplete: "legacy" (default) or "new" for keys with Places API (New) enabled. Uses the same apikey.
zendesk: # We can use different support tools, for now we use Zendesk but others can be integrated
url: "yourdomain.zendesk.com"
posthog: # Posthog is used for auditing and user interaction logging
@@ -676,6 +759,9 @@ kerberoshub:
organization: "github-organization"
team: "github-team"
cleanup:
# Explicit opt-in for a selected hardened image; legacy defaults are unchanged.
securityContext: {}
podSecurityContext: {}
repository: ghcr.io/uug-ai/hub-cleanup
pullPolicy: IfNotPresent
tag: "v1.4.19"
@@ -718,6 +804,10 @@ kerberoshub:
# recording plans, inactive-account cleanup, and the optional global pass.
# Set to "0" or a negative value to retain audit events indefinitely.
auditEventRetentionDays: "400"
# Deployment-wide cap for workflow runs, anchored on run execution time.
# Runs always expire with their media; this only shortens their lifetime.
# Set to "0" or a negative value to let runs follow media retention only.
workflowRunRetentionDays: "0"
resources:
requests:
memory: 10Mi
@@ -809,7 +899,9 @@ kerberoshub:
# triggers how a run OPENS. Omit for a single bare automatic trigger
# (opens for every recording); the per-stage `needs` then decide
# which stages FIRE. Narrow with device/schedule triggers, e.g.
# `- {type: automatic, devices: [{key: <device-key>}]}`.
# `- {type: automatic, devices: [{key: <device-key>}]}`, or by
# device label across organisations, e.g.
# `- {type: automatic, systemTags: [anpr-pilot]}` (see deviceTags).
# stages the executable stages, each {operation, dispatch?, needs?,
# needsMode?}. dispatch is "always" (default) or "conditional";
# a conditional stage's `needs` are upstream dependencies, each
@@ -852,9 +944,10 @@ kerberoshub:
# stages:
# - operation: vlm
# dispatch: always
# Stages tenants may place on the workflow editor canvas next to the device
# node. Each entry needs a deployed worker (services.<operation>.queue);
# entries without one are hidden. Empty means the editor offers no stages.
# Optional editor metadata (name, description, params) per operation. With
# workflowConfiguration contracts, a stage is offered when it has a valid
# contract and a deployed worker (services.<operation>.queue); contract
# presentation and params take precedence over these entries.
editorStages: []
# - operation: vlm
# name: Describe recording
@@ -882,6 +975,8 @@ kerberoshub:
# `enabled` here.
workflows:
repository: ghcr.io/uug-ai/hub-workflows
securityContext: {}
podSecurityContext: {}
pullPolicy: IfNotPresent
tag: "v1.0.0"
replicas: 1 # Number of pods for the service.
@@ -990,12 +1085,18 @@ kerberoshub:
# memory: 128Mi
# cpu: 100m
monitordevice:
securityContext: {}
podSecurityContext: {}
repository: ghcr.io/uug-ai/hub-monitor-device
pullPolicy: IfNotPresent
tag: "v1.4.2"
replicas: 1 # Number of pods for the service.
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
logLevel: "info" # possible values: trace, debug, info, warn, error
# Seconds between device status scans. Sets how quickly a camera going
# offline/online is detected and, with "Group notifications" on, how long
# notifications are held to collect other cameras that change at the same time.
scanIntervalSeconds: 60
resources:
requests:
memory: 10Mi
@@ -1127,11 +1228,14 @@ kerberospipeline:
#volumeMounts:
# - name: extra
# mountPath: /data
logLevel: "info" # possible values: debug, info, warn, error
resources:
requests:
memory: 10Mi
cpu: 10m
throttler:
securityContext: {}
podSecurityContext: {}
repository: uugai/hub-pipeline-throttler
deploymentAnnotations: {}
# deploymentAnnotations:
@@ -1153,6 +1257,8 @@ kerberospipeline:
memory: 10Mi
cpu: 10m
notify:
securityContext: {}
podSecurityContext: {}
repository: ghcr.io/uug-ai/hub-pipeline-notification
deploymentAnnotations: {}
# deploymentAnnotations:
@@ -1201,7 +1307,7 @@ kerberospipeline:
# deploymentAnnotations:
# argocd.argoproj.io/sync-wave: "40"
pullPolicy: IfNotPresent
tag: "v1.8.5"
tag: "v1.8.22"
replicas: 1 # Number of pods for the service.
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
# Optional extra volumes / volumeMounts for this deployment (empty = none).
@@ -1212,11 +1318,28 @@ kerberospipeline:
# - name: extra
# mountPath: /data
logLevel: "info" # possible values: trace, debug, info, warn, error
# Opt-in: after classification, re-enqueue the recording on the thumbnail
# queue to regenerate its thumbnail from the first frame of a tracked moving
# object (THUMBNAIL_REGENERATION_ENABLED). Adds one thumbnail job per
# classified recording; size pipe-thumbnail accordingly before enabling.
thumbnailRegeneration:
enabled: false
# In-memory cache for hot-path MongoDB lookups (site vault per device).
# Site changes apply after at most one TTL; each replica caches separately.
dbCache:
enabled: false
ttl: "5m" # Go duration, e.g. "30s", "5m"
resources:
requests:
memory: 10Mi
cpu: 10m
dominantColor:
securityContext: {}
podSecurityContext: {}
# Opt-in: deploys pipe-dominantcolor and tells the analysis stage to schedule
# it (DOMINANTCOLOR_ENABLED). Requires an analysis image that reads the flag;
# older images always publish to the dominantcolor queue.
enabled: false # Enable or disable dominant color extraction 'true' or 'false'
repository: ghcr.io/uug-ai/hub-pipeline-dominantcolors
deploymentAnnotations: {}
# deploymentAnnotations:
@@ -1241,13 +1364,16 @@ kerberospipeline:
memory: 2Gi
cpu: 1000m
thumbnail:
securityContext: {}
podSecurityContext: {}
repository: ghcr.io/uug-ai/hub-pipeline-thumbnail
deploymentAnnotations: {}
# deploymentAnnotations:
# argocd.argoproj.io/sync-wave: "50"
pullPolicy: IfNotPresent
tag: "v1.3.10"
tag: "v1.3.15"
replicas: 2 # Number of pods for the service.
prefetchCount: 0 # Max unacknowledged RabbitMQ messages per pod; 0 = service default (5).
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
# Optional extra volumes / volumeMounts for this deployment (empty = none).
#volumes:
@@ -1260,6 +1386,7 @@ kerberospipeline:
quality: "1" # 1 (best) - 31 (worst)
width: "600"
height: "-1"
skipNonKeyframes: true # Decode keyframes only for faster extraction; disable when exact-frame output is required.
kerberosvault:
enabled: true # If you want to use Kerberos Vault to store the thumbnails
resources:
@@ -1270,6 +1397,8 @@ kerberospipeline:
memory: 2Gi
cpu: 1000m
counting:
securityContext: {}
podSecurityContext: {}
repository: uugai/hub-pipeline-counting
deploymentAnnotations: {}
# deploymentAnnotations:
@@ -1297,8 +1426,9 @@ kerberospipeline:
# argocd.argoproj.io/sync-wave: "50"
repository: ghcr.io/uug-ai/hub-pipeline-sprite
pullPolicy: IfNotPresent
tag: "v1.1.16"
tag: "v1.1.21"
replicas: 5 # Number of pods for the service.
prefetchCount: 0 # Max unacknowledged RabbitMQ messages per pod; 0 = service default (5).
topologySpreadConstraints: [] # Optional pod topology spread constraints (empty = none).
# Optional extra volumes / volumeMounts for this deployment (empty = none).
#volumes:
@@ -1376,5 +1506,9 @@ email:
disabled: "disable"
highupload: "highupload"
device: "device"
# Sent instead of "device" when several cameras of an account change status
# in the same scan and the account groups device notifications.
deviceGroup: "device_group"
alertTitle: "[Alert] Kerberos Hub detected something an event"
deviceTitle: "[Device] A Kerberos Agent's status has been changed"
deviceGroupTitle: "[Device] Several cameras changed status"
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
set -euo pipefail
CHART_DIR="${1:-charts/hub}"
resource() {
local source="$1"
awk -v source="# Source: hub/templates/admin/$source.yaml" '
/^---/ { selected=0 }
/^# Source:/ { selected=($0 == source); next }
selected { print }
'
}
assert_contains() {
local rendered="$1" pattern="$2"
if ! grep -Eq -- "$pattern" <<< "$rendered"; then
printf 'Missing expected Admin setting: %s\n' "$pattern" >&2
return 1
fi
}
assert_absent() {
local rendered="$1" pattern="$2"
if grep -Eq -- "$pattern" <<< "$rendered"; then
printf 'Unexpected Admin setting: %s\n' "$pattern" >&2
return 1
fi
}
HARDENED_FLAGS=(
--set admin.containerPort=8080
--set-json 'admin.securityContext={"runAsNonRoot":true,"runAsUser":65532,"runAsGroup":65532,"readOnlyRootFilesystem":true,"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"seccompProfile":{"type":"RuntimeDefault"}}'
--set-json 'admin.volumes=[{"name":"admin-tmp","emptyDir":{"medium":"Memory","sizeLimit":"16Mi"}}]'
--set-json 'admin.volumeMounts=[{"name":"admin-tmp","mountPath":"/tmp"}]'
--set-json 'admin.readinessProbe={"httpGet":{"path":"/healthz","port":"http"},"periodSeconds":10}'
--set-json 'admin.livenessProbe={"httpGet":{"path":"/healthz","port":"http"},"initialDelaySeconds":10,"periodSeconds":10}'
)
for mode in all ui; do
default_render="$(helm template hub "$CHART_DIR" --set "mode=$mode" --set admin.oauth2Proxy.enabled=true)"
default_deployment="$(resource deployment <<< "$default_render")"
default_service="$(resource service <<< "$default_render")"
default_ingress="$(resource ingress <<< "$default_render")"
default_oauth="$(resource oauth2-proxy <<< "$default_render")"
assert_contains "$default_deployment" 'containerPort: 80$'
assert_absent "$default_deployment" 'securityContext:|readinessProbe:|livenessProbe:'
assert_contains "$default_service" 'port: 80$'
assert_contains "$default_service" 'targetPort: http$'
hardened_render="$(helm template hub "$CHART_DIR" --set "mode=$mode" --set admin.oauth2Proxy.enabled=true "${HARDENED_FLAGS[@]}")"
hardened_deployment="$(resource deployment <<< "$hardened_render")"
assert_contains "$hardened_deployment" 'containerPort: 8080$'
assert_absent "$hardened_deployment" 'containerPort: 80$'
for setting in 'runAsNonRoot: true' 'runAsUser: 65532' 'runAsGroup: 65532' \
'readOnlyRootFilesystem: true' 'allowPrivilegeEscalation: false' \
'type: RuntimeDefault' 'drop:' '- ALL' 'medium: Memory' \
'sizeLimit: 16Mi' 'mountPath: /tmp' 'name: admin-tmp'; do
assert_contains "$hardened_deployment" "^[[:space:]]*(- )?$setting$"
done
for probe in readinessProbe livenessProbe; do
probe_render="$(awk -v probe="$probe:" '
/^ [a-zA-Z]/ { selected=($1 == probe) }
selected { print }
' <<< "$hardened_deployment")"
assert_contains "$probe_render" 'path: /healthz$'
assert_contains "$probe_render" 'port: http$'
done
[[ "$(resource service <<< "$hardened_render")" == "$default_service" ]]
[[ "$(resource ingress <<< "$hardened_render")" == "$default_ingress" ]]
[[ "$(resource oauth2-proxy <<< "$hardened_render")" == "$default_oauth" ]]
assert_contains "$default_ingress" 'nginx.ingress.kubernetes.io/auth-url:'
assert_contains "$default_ingress" 'number: 80$'
echo "OK ($mode): legacy defaults; hardened runtime/probes; stable Service, ingress and OAuth2"
done
pipeline_render="$(helm template hub "$CHART_DIR" --set mode=pipeline "${HARDENED_FLAGS[@]}")"
assert_absent "$pipeline_render" '^# Source: hub/templates/admin/'
echo "All Admin runtime checks passed."
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env bash
set -euo pipefail
CHART_DIR="${1:-charts/hub}"
components=(
kerberospipeline.counting kerberospipeline.throttler
kerberospipeline.notify
kerberospipeline.thumbnail kerberospipeline.dominantColor
kerberoshub.cleanup kerberoshub.monitordevice
kerberoshub.services.workflows kerberoshub.services.loitering
)
fixtures=(
--set kerberospipeline.dominantColor.enabled=true
--set kerberoshub.workflows.enabled=true
--set kerberoshub.services.loitering.enabled=true
--set kerberoshub.services.loitering.repository=example/loitering
--set kerberoshub.services.loitering.tag=test
--set kerberoshub.services.loitering.queue=loitering
--set mongodb.tls.enabled=true
--set mongodb.tls.existingSecret=mongodb-ca
--set mongodb.tls.caFileName=ca.crt
)
security=()
for component in "${components[@]}"; do
security+=(
--set "$component.securityContext.runAsNonRoot=true"
--set "$component.securityContext.runAsUser=65532"
--set "$component.securityContext.runAsGroup=65532"
--set "$component.securityContext.readOnlyRootFilesystem=true"
--set "$component.securityContext.allowPrivilegeEscalation=false"
--set "$component.securityContext.capabilities.drop[0]=ALL"
--set "$component.securityContext.seccompProfile.type=RuntimeDefault"
--set "$component.podSecurityContext.fsGroup=65532"
--set "$component.volumes[0].name=scratch"
--set "$component.volumes[0].emptyDir.sizeLimit=4Gi"
--set "$component.volumeMounts[0].name=scratch"
--set "$component.volumeMounts[0].mountPath=/tmp"
--set "$component.resources.requests.ephemeral-storage=4Gi"
--set "$component.resources.limits.ephemeral-storage=6Gi"
)
done
resource() {
local kind="$1" name="$2"
awk -v kind="$kind" -v name="$name" '
function emit() { if (matchedKind && matchedName) printf "%s", block }
/^---$/ { emit(); block=""; matchedKind=0; matchedName=0 }
{ block=block $0 "\n" }
$0 == "kind: " kind { matchedKind=1 }
$0 == " name: " name { matchedName=1 }
END { emit() }
'
}
assert_line() {
local block="$1" pattern="$2"
if ! grep -Eq "$pattern" <<< "$block"; then
printf 'Missing expected pattern: %s\n%s\n' "$pattern" "$block" >&2
exit 1
fi
}
for mode in all pipeline ui; do
defaults="$(helm template hub "$CHART_DIR" --set "mode=$mode")"
if grep -q 'securityContext:' <<< "$defaults"; then
echo "Default $mode mode unexpectedly enables security settings" >&2
exit 1
fi
legacy="$(helm template hub "$CHART_DIR" --set "mode=$mode" "${fixtures[@]}")"
hardened="$(helm template hub "$CHART_DIR" --set "mode=$mode" "${fixtures[@]}" "${security[@]}")"
helm lint "$CHART_DIR" --quiet --set "mode=$mode" "${fixtures[@]}" "${security[@]}"
names=()
if [[ "$mode" != ui ]]; then
names+=(pipe-counting pipe-throttler pipe-notify pipe-thumbnail pipe-dominantcolor hub-workflows hub-loitering)
fi
if [[ "$mode" != pipeline ]]; then
names+=(hub-cleanup hub-monitor-device)
fi
for name in "${names[@]}"; do
block="$(resource Deployment "$name" <<< "$hardened")"
for pattern in \
'^[ ]{6}securityContext:$' '^[ ]{8}fsGroup: 65532$' \
'^[ ]+(runAsUser|runAsGroup): 65532$' \
'^[ ]+runAsNonRoot: true$' '^[ ]+readOnlyRootFilesystem: true$' \
'^[ ]+allowPrivilegeEscalation: false$' '^[ ]+capabilities:$' \
'^[ ]+drop:$' '^[ ]+- ALL$' '^[ ]+seccompProfile:$' \
'^[ ]+type: RuntimeDefault$' '^[ ]+(- )?mountPath: /tmp$' \
'^[ ]+sizeLimit: 4Gi$' '^[ ]+ephemeral-storage: 6Gi$' \
'^[ ]+ephemeral-storage: 4Gi$'; do
assert_line "$block" "$pattern"
done
# Verify both identity axes independently.
assert_line "$block" '^[ ]+runAsUser: 65532$'
assert_line "$block" '^[ ]+runAsGroup: 65532$'
assert_line "$block" '^[ ]{8,10}securityContext:$'
if [[ "$name" == hub-cleanup || "$name" == hub-monitor-device || "$name" == hub-workflows || "$name" == pipe-notify ]]; then
assert_line "$block" '^[ ]+secretName: mongodb-ca$'
assert_line "$block" '^[ ]+mountPath: /etc/mongodb/tls$'
assert_line "$block" '^[ ]+readOnly: true$'
fi
if [[ "$name" == pipe-notify ]]; then
# Provider, queue, FFmpeg and all other worker settings are unchanged.
old_block="$(resource Deployment pipe-notify <<< "$legacy")"
[[ "$(sed -n '/^ env:/,$p' <<< "$old_block")" == "$(sed -n '/^ env:/,$p' <<< "$block")" ]]
fi
if [[ "$name" != hub-monitor-device ]]; then
service="$(resource Service "$name" <<< "$hardened")"
assert_line "$service" '^[ ]+port: 8080$'
assert_line "$service" '^[ ]+targetPort: 8080$'
fi
done
expected="${#names[@]}"
[[ "$(grep -c 'runAsUser: 65532' <<< "$hardened")" == "$expected" ]]
[[ "$(grep -c 'fsGroup: 65532' <<< "$hardened")" == "$expected" ]]
# Image selection, Services, env and TLS mounts must not change.
for pattern in '^[ ]+image:' '^[ ]+(port|targetPort|containerPort):' 'mongodb-tls'; do
[[ "$(grep -E "$pattern" <<< "$legacy")" == "$(grep -E "$pattern" <<< "$hardened")" ]]
done
echo "OK ($mode): defaults unchanged; $expected independent hardened workers preserve images, ports and MongoDB TLS"
done
# Hardening one worker must not opt in another, even on the same pod mode.
isolated="$(helm template hub "$CHART_DIR" --set mode=pipeline \
--set kerberospipeline.counting.securityContext.runAsUser=65532)"
[[ "$(grep -c 'runAsUser: 65532' <<< "$isolated")" == 1 ]]
assert_line "$(resource Deployment pipe-counting <<< "$isolated")" '^[ ]+runAsUser: 65532$'
echo "All hardened worker checks passed."
admin_flags=(
--set admin.containerPort=8080
--set admin.securityContext.runAsNonRoot=true
--set admin.securityContext.runAsUser=65532
--set admin.securityContext.runAsGroup=65532
--set admin.securityContext.readOnlyRootFilesystem=true
--set admin.securityContext.allowPrivilegeEscalation=false
--set admin.securityContext.capabilities.drop[0]=ALL
--set admin.securityContext.seccompProfile.type=RuntimeDefault
--set admin.volumes[0].name=admin-tmp
--set admin.volumes[0].emptyDir.medium=Memory
--set admin.volumes[0].emptyDir.sizeLimit=16Mi
--set admin.volumeMounts[0].name=admin-tmp
--set admin.volumeMounts[0].mountPath=/tmp
--set admin.oauth2Proxy.enabled=true
)
for mode in all ui pipeline; do
legacy="$(helm template hub "$CHART_DIR" --set "mode=$mode" --set admin.oauth2Proxy.enabled=true)"
configured="$(helm template hub "$CHART_DIR" --set "mode=$mode" "${admin_flags[@]}")"
helm lint "$CHART_DIR" --quiet --set "mode=$mode" "${admin_flags[@]}"
if [[ "$mode" == pipeline ]]; then
[[ -z "$(resource Deployment admin <<< "$configured")" ]]
[[ -z "$(resource Service admin-svc <<< "$configured")" ]]
[[ "$legacy" == "$configured" ]]
echo "OK (pipeline): Admin settings render no Admin resources or unrelated changes"
continue
fi
old="$(resource Deployment admin <<< "$legacy")"
block="$(resource Deployment admin <<< "$configured")"
assert_line "$old" '^[ ]+containerPort: 80$'
[[ "$old" != *securityContext:* && "$old" != *admin-tmp* ]]
for pattern in \
'^[ ]+containerPort: 8080$' '^[ ]{10}securityContext:$' \
'^[ ]+runAsUser: 65532$' '^[ ]+runAsGroup: 65532$' \
'^[ ]+runAsNonRoot: true$' '^[ ]+readOnlyRootFilesystem: true$' \
'^[ ]+allowPrivilegeEscalation: false$' '^[ ]+capabilities:$' \
'^[ ]+drop:$' '^[ ]+- ALL$' '^[ ]+seccompProfile:$' \
'^[ ]+type: RuntimeDefault$' '^[ ]+medium: Memory$' \
'^[ ]+sizeLimit: 16Mi$' '^[ ]+(- )?mountPath: /tmp$'; do
assert_line "$block" "$pattern"
done
old_service="$(resource Service admin-svc <<< "$legacy")"
service="$(resource Service admin-svc <<< "$configured")"
assert_line "$old_service" '^[ ]+targetPort: http$'
assert_line "$service" '^[ ]+port: 80$'
assert_line "$service" '^[ ]+targetPort: http$'
[[ "$(resource Ingress admin-ingress <<< "$legacy")" == "$(resource Ingress admin-ingress <<< "$configured")" ]]
[[ "$(resource Deployment oauth2-proxy-admin <<< "$legacy")" == "$(resource Deployment oauth2-proxy-admin <<< "$configured")" ]]
[[ "$(resource Service oauth2-proxy-admin <<< "$legacy")" == "$(resource Service oauth2-proxy-admin <<< "$configured")" ]]
[[ "$(grep 'image:' <<< "$legacy")" == "$(grep 'image:' <<< "$configured")" ]]
# No other workload inherits Admin's container port or security settings.
[[ "$(grep -c 'runAsUser: 65532' <<< "$configured")" == 1 ]]
echo "OK ($mode): Admin opt-in port/security/tmpfs; legacy image, Service port, ingress and OAuth2-proxy preserved"
done
echo "All hardened Admin checks passed."
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
set -euo pipefail
CHART_DIR="${1:-charts/hub}"
extract_passkeys() {
awk '
/^---/ { deployment=0; name=""; variable="" }
/^kind: Deployment$/ { deployment=1 }
deployment && /^ name:/ { name=$2 }
deployment && /- name: PASSKEY_/ { variable=$3; next }
variable != "" && /value:/ {
value=$0
sub(/^[[:space:]]*value: /, "", value)
print name, variable, value
variable=""
}
' | sort
}
assert_passkeys() {
local rendered="$1" expected="$2" actual count expected_count
actual="$(printf '%s\n' "$rendered" | extract_passkeys)"
if [[ "$actual" != "$expected" ]]; then
printf 'Unexpected passkey environment:\n%s\nExpected:\n%s\n' "$actual" "$expected" >&2
return 1
fi
count="$(printf '%s\n' "$rendered" | awk '/- name: PASSKEY_/ { count++ } END { print count+0 }')"
expected_count="$(printf '%s\n' "$expected" | awk 'NF { count++ } END { print count+0 }')"
if [[ "$count" != "$expected_count" ]]; then
echo "Passkey variables leaked outside the expected Deployment or were duplicated" >&2
return 1
fi
}
CONFIGURED_FLAGS=(
--set-json kerberoshub.api.passkeys.enabled=true
--set-json 'kerberoshub.api.passkeys.rpId="example.com"'
--set-json 'kerberoshub.api.passkeys.origins=["https://hub.example.com","https://legacy.example.com:8443"]'
--set-json 'kerberoshub.api.passkeys.rpName="Example Hub"'
)
expected="$(printf '%s\n' \
'hub-api PASSKEY_ORIGINS "https://hub.example.com,https://legacy.example.com:8443"' \
'hub-api PASSKEY_RP_ID "example.com"' \
'hub-api PASSKEY_RP_NAME "Example Hub"' | sort)"
for mode in all ui pipeline; do
default_render="$(helm template hub "$CHART_DIR" --set "mode=$mode")"
assert_passkeys "$default_render" ""
configured_render="$(helm template hub "$CHART_DIR" --set "mode=$mode" "${CONFIGURED_FLAGS[@]}")"
if [[ "$mode" == pipeline ]]; then
assert_passkeys "$configured_render" ""
else
assert_passkeys "$configured_render" "$expected"
fi
echo "OK ($mode): defaults opt out; enabled settings affect Hub API only"
done
disabled_render="$(helm template hub "$CHART_DIR" "${CONFIGURED_FLAGS[@]}" --set kerberoshub.api.passkeys.enabled=false)"
assert_passkeys "$disabled_render" ""
localhost_render="$(helm template hub "$CHART_DIR" --set mode=ui \
--set kerberoshub.api.passkeys.enabled=true \
--set-string kerberoshub.api.passkeys.rpId=localhost \
--set-json 'kerberoshub.api.passkeys.origins=["http://localhost:4200"]')"
assert_passkeys "$localhost_render" "$(printf '%s\n' \
'hub-api PASSKEY_ORIGINS "http://localhost:4200"' \
'hub-api PASSKEY_RP_ID "localhost"' \
'hub-api PASSKEY_RP_NAME "Kerberos Hub"' | sort)"
legacy_render="$(helm template hub "$CHART_DIR" --set mode=ui \
--set-json 'kerberoshub.extraEnv=[{"name":"PASSKEY_RP_ID","value":"example.com"}]')"
printf '%s\n' "$legacy_render" | grep -q -- '- name: PASSKEY_RP_ID'
expect_invalid() {
local output
if output="$(helm template hub "$CHART_DIR" "${CONFIGURED_FLAGS[@]}" "$@" 2>&1)"; then
echo "Invalid passkey settings unexpectedly rendered: $*" >&2
return 1
fi
if [[ "$output" != *"passkeys"* ]]; then
printf 'Rendering failed for an unexpected reason:\n%s\n' "$output" >&2
return 1
fi
}
expect_invalid --set-string kerberoshub.api.passkeys.rpId=
expect_invalid --set-json 'kerberoshub.api.passkeys.origins=[]'
expect_invalid --set-json 'kerberoshub.api.passkeys.origins=["https://hub.example.com/"]'
expect_invalid --set-json 'kerberoshub.api.passkeys.origins=["http://hub.example.com"]'
expect_invalid --set-json 'kerberoshub.api.passkeys.origins=["https://hub.example.com","https://hub.example.com"]'
expect_invalid --set-json 'kerberoshub.api.passkeys.origins="https://hub.example.com"'
expect_invalid --set-json 'kerberoshub.api.passkeys.enabled="true"'
expect_invalid --set-json 'kerberoshub.api.passkeys.rpId=123'
expect_invalid --set-json 'kerberoshub.api.passkeys.rpName=123'
expect_invalid --set-string kerberoshub.api.passkeys.origin=https://hub.example.com
echo "All Hub passkey configuration checks passed."
@@ -51,21 +51,21 @@ expected="$(printf '%s\n' \
'pipe-counting 50' 'pipe-sequence 60' 'pipe-monitor 70' | sort)"
for mode in all pipeline; do
default_render="$(helm template hub "$CHART_DIR" --set "mode=$mode" --set kerberospipeline.sprite.enabled=true)"
default_render="$(helm template hub "$CHART_DIR" --set "mode=$mode" --set kerberospipeline.sprite.enabled=true --set kerberospipeline.dominantColor.enabled=true)"
assert_waves "$default_render" ""
if printf '%s\n' "$default_render" | grep -q '^ annotations: null'; then
echo "Empty annotations must be omitted" >&2
exit 1
fi
configured_render="$(helm template hub "$CHART_DIR" "${WAVE_FLAGS[@]}" --set "mode=$mode" --set kerberospipeline.sprite.enabled=true)"
configured_render="$(helm template hub "$CHART_DIR" "${WAVE_FLAGS[@]}" --set "mode=$mode" --set kerberospipeline.sprite.enabled=true --set kerberospipeline.dominantColor.enabled=true)"
assert_waves "$configured_render" "$expected"
echo "OK ($mode): defaults opt out; per-service values assign all ten Deployment waves"
done
disabled_sprite="$(helm template hub "$CHART_DIR" "${WAVE_FLAGS[@]}" --set mode=pipeline)"
assert_waves "$disabled_sprite" "$(printf '%s\n' "$expected" | grep -v '^pipe-sprite ')"
disabled_optional="$(helm template hub "$CHART_DIR" "${WAVE_FLAGS[@]}" --set mode=pipeline)"
assert_waves "$disabled_optional" "$(printf '%s\n' "$expected" | grep -v -e '^pipe-sprite ' -e '^pipe-dominantcolor ')"
ui_render="$(helm template hub "$CHART_DIR" "${WAVE_FLAGS[@]}" --set mode=ui --set kerberospipeline.sprite.enabled=true)"
ui_render="$(helm template hub "$CHART_DIR" "${WAVE_FLAGS[@]}" --set mode=ui --set kerberospipeline.sprite.enabled=true --set kerberospipeline.dominantColor.enabled=true)"
assert_waves "$ui_render" ""
# Exercise zero/negative strings and an unrelated annotation on the same map.
-114
View File
@@ -1,114 +0,0 @@
#!/usr/bin/env bash
# Chart-only checks; no cluster or application loader required.
set -euo pipefail
CHART_DIR="${1:-charts/hub}"
assert_contains() {
if ! grep -Fq -- "$2" <<<"$1"; then
printf 'FAIL: expected rendered output to contain %s\n' "$2" >&2
exit 1
fi
}
assert_absent() {
if grep -Fq -- "$2" <<<"$1"; then
printf 'FAIL: unexpected rendered output contains %s\n' "$2" >&2
exit 1
fi
}
expect_failure() {
local expected="$1"
shift
local output
if output="$(helm template hub "$CHART_DIR" "$@" 2>&1)"; then
printf 'FAIL: render unexpectedly succeeded: %s\n' "$*" >&2
exit 1
fi
assert_contains "$output" "$expected"
}
if grep -q '^classificationCatalog:' "$CHART_DIR/values.yaml"; then
echo "FAIL: classificationCatalog should be omitted from default values" >&2
exit 1
fi
default_out="$(helm template hub "$CHART_DIR")"
assert_absent "$default_out" "name: workflow-configuration"
assert_absent "$default_out" "checksum/workflow-configuration"
assert_contains "$default_out" "value: /etc/kerberos/classifications/classifications.json"
legacy_out="$(helm template hub "$CHART_DIR" --set workflowConfiguration=null)"
if [ "$default_out" != "$legacy_out" ]; then
echo "FAIL: absent workflowConfiguration changed the default render" >&2
exit 1
fi
null_out="$(helm template hub "$CHART_DIR" --set classificationCatalog=null)"
if [ "$default_out" != "$null_out" ]; then
echo "FAIL: omitted and null classificationCatalog render differently" >&2
exit 1
fi
# A file-backed default must render identically to the previous inline-list form.
explicit_out="$(helm template hub "$CHART_DIR" \
--set-json "classificationCatalog=$(tr -d '\n' <"$CHART_DIR/configuration/catalogs/classifications.json")")"
if [ "$default_out" != "$explicit_out" ]; then
echo "FAIL: bundled defaults and explicit classification list render differently" >&2
exit 1
fi
override_flags=(--set-json 'classificationCatalog=[{"key":"forklift","label":"Forklift","icon":"vehicle"}]')
override_out="$(helm template hub "$CHART_DIR" "${override_flags[@]}")"
assert_contains "$override_out" '"key": "forklift"'
assert_absent "$override_out" '"key": "pedestrian"'
empty_out="$(helm template hub "$CHART_DIR" --set-json 'classificationCatalog=[]')"
assert_contains "$empty_out" " []"
assert_absent "$empty_out" '"key": "pedestrian"'
disabled_out="$(helm template hub "$CHART_DIR" --set classificationCatalogEnabled=false)"
assert_absent "$disabled_out" "name: classification-catalog"
assert_absent "$disabled_out" "CLASSIFICATION_CATALOG_FILE"
bundle_out="$(helm template hub "$CHART_DIR" --set workflowConfiguration.enabled=true)"
assert_contains "$bundle_out" "checksum/workflow-configuration:"
assert_contains "$bundle_out" "mountPath: /etc/kerberos/configuration"
assert_contains "$bundle_out" "path: workflow-contracts/start.yaml"
assert_contains "$bundle_out" "path: catalogs/classifications.json"
assert_contains "$bundle_out" "source: project.devices"
assert_contains "$bundle_out" "file: ../catalogs/classifications.json"
assert_absent "$bundle_out" "WORKFLOW_CONTRACTS"
bundle_config="$(helm template hub "$CHART_DIR" --set workflowConfiguration.enabled=true \
--show-only templates/configmap-workflow-configuration.yaml)"
assert_absent "$bundle_config" "classifications.json: |-"
override_bundle="$(helm template hub "$CHART_DIR" --set workflowConfiguration.enabled=true "${override_flags[@]}")"
assert_contains "$override_bundle" '"key": "forklift"'
assert_absent "$override_bundle" '"key": "pedestrian"'
external_flags=(-f "$CHART_DIR/examples/workflow-configuration-values.yaml")
external_out="$(helm template hub "$CHART_DIR" "${external_flags[@]}" --set classificationCatalogEnabled=false)"
assert_contains "$external_out" 'name: "custom-workflow-configuration"'
assert_contains "$external_out" "path: workflow-contracts/pose.yaml"
assert_contains "$external_out" "path: catalogs/pose-keypoints.json"
assert_absent "$external_out" "name: hub-workflow-configuration"
assert_absent "$external_out" "checksum/workflow-configuration"
ui_out="$(helm template hub "$CHART_DIR" --set mode=ui --set workflowConfiguration.enabled=true)"
assert_contains "$ui_out" "name: hub-workflow-configuration"
pipeline_out="$(helm template hub "$CHART_DIR" --set mode=pipeline --set workflowConfiguration.enabled=true)"
assert_absent "$pipeline_out" "name: hub-workflow-configuration"
assert_absent "$pipeline_out" "name: workflow-configuration"
expect_failure "requires classificationCatalogEnabled" \
--set workflowConfiguration.enabled=true --set classificationCatalogEnabled=false
expect_failure "items is required" \
--set workflowConfiguration.enabled=true --set workflowConfiguration.existingConfigMap=custom
expect_failure "items requires existingConfigMap" "${external_flags[@]}" --set workflowConfiguration.existingConfigMap=
expect_failure "duplicate workflowConfiguration.items path" "${external_flags[@]}" \
--set 'workflowConfiguration.items[1].path=workflow-contracts/pose.yaml'
expect_failure "workflowConfiguration" "${external_flags[@]}" \
--set 'workflowConfiguration.items[0].path=../../secret.yaml'
expect_failure "workflowConfiguration" "${external_flags[@]}" \
--set 'workflowConfiguration.items[0].path=/etc/secret.yaml'
expect_failure "classificationCatalog" --set classificationCatalog=not-a-list
echo "All workflow configuration and classification catalog checks passed."
@@ -1,233 +0,0 @@
#!/usr/bin/env bash
#
# Render the hub chart and assert that every deployment which carries the
# workflows hand-off queue (the WORKFLOWS_QUEUE env var) resolves to the SAME,
# non-empty value.
#
# Why: the analysis pipeline (pipe-analysis) publishes opened workflow runs to
# WORKFLOWS_QUEUE, the workflows engine (hub-workflows) consumes it, and every
# stage worker (hub-stage) routes its result back to it. All three templates
# read the single key `kerberoshub.services.workflows.queue`. If a future edit
# hardcodes a value, reads the wrong key, or drops the env on one of them, the
# producer and consumer silently drift onto different queue names and messages
# pile up with no consumer. This check fails the build before that can ship.
#
# Also checks workflow identity validation and identical WORKFLOW_DEFINITIONS
# in the API and engine. Requires Helm and Python 3 (standard library only).
#
# Usage: scripts/check-workflows-queue-consistency.sh [chart-dir]
# (chart-dir defaults to charts/hub, relative to the repo root)
set -euo pipefail
CHART_DIR="${1:-charts/hub}"
PROBE="drift-probe-queue-name"
# Flags that force all three deployment kinds (analysis, engine and one stage
# worker) to render, so the check actually has something to compare. The chart
# ships NO enabled stage worker by default (custom stages are values-only and
# opt-in), so we synthesise a throwaway stage purely to exercise the generic
# hub-stage path. The name is a neutral fixture ("queuecheck") on purpose: any
# arbitrary stage key must render the same way, so the check must not depend on
# a specific bundled worker.
STAGE="queuecheck"
RENDER_FLAGS=(
--set mode=all
--set kerberoshub.workflows.enabled=true
--set "kerberoshub.workflows.stages.${STAGE}.enabled=true"
--set "kerberoshub.services.${STAGE}.enabled=true"
--set "kerberoshub.services.${STAGE}.repository=example.invalid/queuecheck"
--set "kerberoshub.services.${STAGE}.tag=test"
--set "kerberoshub.services.${STAGE}.queue=queuecheck-fixture-queue"
)
# Read `helm template` output on stdin and print one WORKFLOWS_QUEUE value per
# line. Matches the `- name: WORKFLOWS_QUEUE` env entry and captures the value
# from the following `value:` line, skipping blank/comment lines in between.
extract_workflows_queue() {
awk '
/^[[:space:]]*-[[:space:]]*name:[[:space:]]*WORKFLOWS_QUEUE[[:space:]]*$/ { want=1; next }
want==1 {
if ($0 ~ /^[[:space:]]*#/ || $0 ~ /^[[:space:]]*$/) next
v=$0
sub(/^[[:space:]]*value:[[:space:]]*/, "", v)
sub(/^"/, "", v); sub(/"[[:space:]]*$/, "", v)
sub(/[[:space:]]+$/, "", v)
print v
want=0
}
'
}
assert_all_equal() {
local expected="$1"; shift
local label="$1"; shift
local -a vals=("$@")
if [ "${#vals[@]}" -lt 2 ]; then
echo "FAIL (${label}): expected at least 2 WORKFLOWS_QUEUE values (analysis + engine), found ${#vals[@]}" >&2
return 1
fi
local v
for v in "${vals[@]}"; do
if [ -z "${v}" ]; then
echo "FAIL (${label}): a deployment rendered an empty WORKFLOWS_QUEUE value" >&2
return 1
fi
if [ "${v}" != "${expected}" ]; then
echo "FAIL (${label}): WORKFLOWS_QUEUE drift detected — expected '${expected}' but a deployment rendered '${v}'" >&2
printf ' rendered values: %s\n' "${vals[*]}" >&2
return 1
fi
done
echo "OK (${label}): ${#vals[@]} deployments all use WORKFLOWS_QUEUE='${expected}'"
}
echo "== Rendering ${CHART_DIR} with the chart's default workflows queue =="
default_out="$(helm template hub "${CHART_DIR}" "${RENDER_FLAGS[@]}")"
mapfile -t default_vals < <(printf '%s\n' "${default_out}" | extract_workflows_queue)
default_queue="${default_vals[0]:-}"
assert_all_equal "${default_queue}" "default values" "${default_vals[@]}" || exit 1
echo "== Rendering ${CHART_DIR} with an overridden workflows queue (-> ${PROBE}) =="
probe_out="$(helm template hub "${CHART_DIR}" "${RENDER_FLAGS[@]}" \
--set kerberoshub.services.workflows.queue="${PROBE}")"
mapfile -t probe_vals < <(printf '%s\n' "${probe_out}" | extract_workflows_queue)
assert_all_equal "${PROBE}" "override probe" "${probe_vals[@]}" || exit 1
assert_definitions() {
local rendered="$1" expected="$2" label="$3"
printf '%s\n' "$rendered" | python3 -c '
import json
import re
import sys
expected = json.loads(sys.argv[1])
label = sys.argv[2]
actual = {}
for document in sys.stdin.read().split("\n---"):
match = re.search(r"(?m)^[ \t]*- name: WORKFLOW_DEFINITIONS[ \t]*\n[ \t]*value: (\".*\")$", document)
if not match:
continue
name = re.search(r"(?m)^metadata:\n name: (.+)$", document).group(1)
assert name not in actual, f"duplicate WORKFLOW_DEFINITIONS in {name}"
actual[name] = json.loads(json.loads(match.group(1)))
assert set(actual) == {"hub-api", "hub-workflows"}, f"unexpected deployments: {set(actual)}"
for name, definitions in actual.items():
assert definitions == expected, f"{label}: {name}: {definitions!r} != {expected!r}"
print(f"OK ({label}): API and engine receive identical expected workflow definitions")
' "$expected" "$label"
}
assert_render_failure() {
local label="$1" expected="$2"; shift 2
local output
if output="$(helm template hub "${CHART_DIR}" "${RENDER_FLAGS[@]}" "$@" 2>&1)"; then
echo "FAIL (${label}): Helm accepted invalid workflow identities" >&2
return 1
fi
if [[ "$output" != *"$expected"* ]]; then
printf 'FAIL (%s): expected diagnostic %s, got:\n%s\n' "$label" "$expected" "$output" >&2
return 1
fi
echo "OK (${label}): Helm rejected invalid workflow identities"
}
assert_definitions "$default_out" '[]' "empty defaults"
echo "== Checking explicit and legacy workflow definitions =="
identity_fixture='{
" Legacy Workflow ": {
"enabled": true,
"stages": [
{"operation": "queuecheck"},
{"operation": "external", "dispatch": "conditional", "needs": [{"operation": "queuecheck"}], "needsMode": "all"}
]
},
"explicit": {
"id": "ABCDEF0123456789ABCDEF01",
"enabled": true,
"triggers": [{"type": "automatic", "devices": [{"key": "fixture-device"}]}],
"stages": []
},
"disabled": {"id": "abcdef0123456789abcdef02", "enabled": false}
}'
expected_definitions='[
{
"name": " Legacy Workflow ", "enabled": true, "source": "config",
"triggers": [{"type": "automatic"}],
"stages": [
{"operation": "queuecheck", "dispatch": "always", "queue": "queuecheck-fixture-queue"},
{"operation": "external", "dispatch": "conditional", "needs": [{"operation": "queuecheck"}], "needsMode": "all"}
]
},
{
"name": "explicit", "id": "abcdef0123456789abcdef01", "enabled": true, "source": "config",
"triggers": [{"type": "automatic", "devices": [{"key": "fixture-device"}]}],
"stages": []
}
]'
identity_out="$(helm template hub "${CHART_DIR}" "${RENDER_FLAGS[@]}" \
--set-json "kerberoshub.workflows.definitions=$identity_fixture")"
assert_definitions "$identity_out" "$expected_definitions" "explicit ID lowercase; legacy omission; disabled filtering; triggers and stages unchanged"
renamed_out="$(helm template hub "${CHART_DIR}" "${RENDER_FLAGS[@]}" \
--set-json "kerberoshub.workflows.definitions=${identity_fixture/\"explicit\"/\"renamed\"}")"
assert_definitions "$renamed_out" "${expected_definitions/\"explicit\"/\"renamed\"}" "explicit identity survives rename"
legacy_only_out="$(helm template hub "${CHART_DIR}" "${RENDER_FLAGS[@]}" \
--set-json 'kerberoshub.workflows.definitions={"legacy":{"enabled":true}}')"
assert_definitions "$legacy_only_out" \
'[{"name":"legacy","enabled":true,"source":"config","triggers":[{"type":"automatic"}],"stages":[]}]' \
"legacy-only values unchanged"
echo "== Checking bundled example identity pins =="
python3 - "$CHART_DIR/values.yaml" <<'PY'
import hashlib
import pathlib
import re
import sys
values = pathlib.Path(sys.argv[1]).read_text()
for name in ("tracking-workflow", "vlm-workflow"):
expected = hashlib.sha256(name.encode()).hexdigest()[:24]
match = re.search(r"(?m)^ # " + re.escape(name) + r':\n # id: "([^"]+)"$', values)
assert match and match.group(1) == expected, f"{name} must preserve its name-derived ID {expected}"
print(f"OK ({name}): example pins existing identity {expected}")
PY
echo "== Rejecting invalid explicit IDs, including disabled definitions =="
invalid_ids=('""' 'null' '"000000000000000000000000"' '0' '123' 'true' 'false' '[]' '{}'
'"abc"' '"zzzzzzzzzzzzzzzzzzzzzzzz"' '"abcdef0123456789abcdef01 "'
'"abcdef0123456789abcdef0"' '"abcdef0123456789abcdef012"')
invalid_diagnostic='kerberoshub.workflows.definitions["invalid"].id must be a non-zero 24-character hexadecimal string'
for enabled in true false; do
for id in "${invalid_ids[@]}"; do
assert_render_failure "id=$id, enabled=$enabled" "$invalid_diagnostic" \
--set-json "kerberoshub.workflows.definitions={\"invalid\":{\"enabled\":$enabled,\"id\":$id}}"
done
done
# Validation must not depend on either consumer being rendered.
for mode in all ui pipeline; do
assert_render_failure "engine disabled, mode=$mode" "$invalid_diagnostic" \
--set "mode=$mode" --set kerberoshub.workflows.enabled=false \
--set-json 'kerberoshub.workflows.definitions={"invalid":{"enabled":false,"id":null}}'
done
echo "== Rejecting duplicate effective IDs =="
legacy_id="$(python3 -c 'import hashlib; print(hashlib.sha256(b" Legacy Workflow ").hexdigest()[:24])')"
for first_enabled in true false; do
for second_enabled in true false; do
assert_render_failure "explicit collision, enabled=$first_enabled/$second_enabled" \
'duplicate effective workflow id "abcdef0123456789abcdef01" for "first" and "second"' \
--set-json "kerberoshub.workflows.definitions={\"first\":{\"enabled\":$first_enabled,\"id\":\"ABCDEF0123456789ABCDEF01\"},\"second\":{\"enabled\":$second_enabled,\"id\":\"abcdef0123456789abcdef01\"}}"
assert_render_failure "explicit/derived collision, enabled=$first_enabled/$second_enabled" \
"duplicate effective workflow id \"$legacy_id\" for \" Legacy Workflow \" and \"explicit\"" \
--set-json "kerberoshub.workflows.definitions={\" Legacy Workflow \":{\"enabled\":$first_enabled},\"explicit\":{\"enabled\":$second_enabled,\"id\":\"${legacy_id^^}\"}}"
done
done
echo "All workflow queue and identity checks passed."