Skip to content

Commit ce99c3a

Browse files
auyidi1CopilotWilliamBerryiii
authored
feat(application): add 507 snapshot normalizer MQTT adapter and chart (#866)
# Pull Request ## Description > **Depends on #867.** Merge #867 first: it makes the 507 inference service subscribe to the pinned `edge-ai/v1/+/camera/+/snapshots` filter this adapter publishes to. Adds the component 507 **snapshot normalizer**: an MQTT adapter that subscribes to a binary JPEG snapshot topic, such as a media connector `snapshot-to-mqtt` stream, and republishes each accepted snapshot as an `image_snapshot` v1 request for the 507 inference service. It's built on the transport-free `snapshot-normalizer-core` library from #831 and follows the custom workload messaging contract from #852. * **New crate `services/snapshot-normalizer`.** It's a binary crate that depends on the core library by path, so the core stays transport-free as its README promises. * **Versioned output topic.** The default is `edge-ai/v1/snapshot-normalizer/camera/{camera-id}/snapshots`, following the #852 `{domain}/{version}/{producer}/{resource-kind}/{resource-id}/{message-kind}` grammar pinned to `v1`. It matches 507's pinned `edge-ai/v1/+/camera/+/snapshots` filter, which #867 adds to the binary default and the shipped manifests alongside the legacy 5-level filter; #867 also rejects envelopes whose `schema_version` major isn't `1`. The adapter refuses to start when its output topic matches its own input filter, including through wildcards. * **CloudEvents attributes as MQTTv5 user properties** come from the SDK's validated `CloudEventBuilder`: `specversion`, `type`, `source`, `id`, `time`, `subject`, `datacontenttype` (also the MQTTv5 Content Type), an optional `dataschema`, and a propagated W3C `traceparent`. * **Producer-keyed deduplication (FR-01).** The key is the input's CloudEvents `id`, scoped to the source topic and `source`, and bounded by both entry count and time window. Content is never a key. A key is recorded only after the PUBACK. Output `id` and `correlation_id` are random UUIDv4 values, per #852, never derived from producer identifiers. * **Delivery (at least once).** Inputs are acknowledged manually after handling. Publishes get up to `PUBLISH_ATTEMPTS` (1-10) attempts with exponential backoff capped at 30 s; a broker failure reason is not retried. Failed publishes are counted and dropped rather than left unacknowledged, because the SDK delivers acks in receive order. Only an input left unacknowledged by a restart is redelivered, and it can then be published twice. * **Session.** The adapter fails at startup when the SUBACK carries a failure reason, so the pod crash-loops visibly. It advertises a receive maximum of 8 and a maximum packet size of `MAX_JPEG_BYTES` plus 64 KiB, and subscribes with retain handling "do not send" so retained snapshots aren't replayed. * **Payload-safe logging.** Logs contain counters, reasons, and lengths only; no payload, Base64 data, or producer identifiers. * **Helm chart `charts/snapshot-normalizer` (FR-05):** * A single-replica StatefulSet, so `AIO_MQTT_CLIENT_ID` (prefix plus `metadata.name` through the Downward API) is unique per release and stable at `<fullname>-0` across restarts, and the persistent session resumes. * `AIO_MQTT_SESSION_EXPIRY` set explicitly from `mqtt.sessionExpirySeconds` (3600 by default). * A dedicated service account without an automounted API token, a projected `aio-internal` SAT over TLS, and the `aio-broker-auth/workload` attribute annotation for least-privilege rules. * No ports or probes; a read-only root filesystem with dropped capabilities and `RuntimeDefault` seccomp. * A placeholder image registry. * **507 consumer fix.** `IncomingMessage::ImageSnapshot.metadata` now defaults when absent. The `image_snapshot` v1 schema makes `metadata` optional and the core omits it when empty, but the consumer rejected such messages with `missing field 'metadata'`. A regression test deserializes the schema examples and a normalizer-shaped envelope without `metadata`. * **Local compose.** The `snapshot-normalizer` service loads `services/snapshot-normalizer/.env.template`, then an optional git-ignored `.env`, instead of hard-coded values. The inference chart hardening items (FR-02 model paths and readiness, FR-03 probe-only listener, FR-04 metrics port) are left to a separate follow-up PR. ## Related Issue Follows up #831 (PCM-03 packaging) and applies the custom workload messaging contract from #852 (#835). ## Type of Change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Blueprint modification or addition - [x] Component modification or addition - [x] Documentation update - [x] CI/CD pipeline change - [ ] Other (please describe): ## Implementation Details * Uses `azure_iot_operations_mqtt` 1.2.3: `recv_manual_ack`, `TopicName::matches_topic_filter`, and `aio::cloud_event`. Other 507 and 50x crates are unchanged. * `config.rs` reads configuration through an injected lookup. It validates the camera ID as a lowercase URL-safe topic segment, the topics, the CloudEvents `source` and `dataschema`, and positive numeric bounds. * `pipeline.rs` is pure: `prepare()` returns `Rejected`, `Duplicate`, or `Publish` without I/O. `main.rs` only owns the session, acks, retries, counters, and SIGTERM handling. * The chart renders numeric values through `int64`, so large byte limits aren't emitted in float notation (for example `4.194304e+06`). * Registered the crate in both `rust-tests.yml` matrices. `codecov.yml` already covers `507-ai-inference/**`. * Made the existing root-absolute `CONTRIBUTING.md` link in the 507 README relative so the link checker resolves it. ## Testing Performed - [ ] Terraform plan/apply - [ ] Blueprint deployment test - [x] Unit tests - [x] Integration tests - [x] Bug fix includes regression test (see [Test Policy](docs/contributing/testing-validation.md)) - [x] Manual validation - [x] Other: container image build and smoke test * `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, and `cargo test` pass: 33 tests for the new crate, including SUBACK rejection, retain handling, capped backoff, the `PUBLISH_ATTEMPTS` bound, random v4 IDs, and the v1 topic and loop guard. The `ai-edge-inference` crate passes `cargo clippy --all-targets -- -D warnings` and its 8 tests with the `metadata` fix. * A local merge of this branch onto #867 is conflict-free and passes the combined `ai-edge-inference` tests, clippy, `docker compose config`, and `kubectl kustomize`. * **End-to-end against an MQTTv5 broker (Mosquitto):** * 5 inputs produced 3 published requests, 1 duplicate (same `id`), 1 `unkeyed`, and 1 `not_jpeg` rejection. * Output arrived on the inference service's versioned input filter with the expected CloudEvents properties. This end-to-end pass predates the review follow-up commit that moved the topic to `edge-ai/v1/snapshot-normalizer/camera/{camera-id}/snapshots`. * Producer `time` and `traceparent` were propagated. * A standalone serde probe of the 507 `IncomingMessage` confirmed that an envelope without `metadata` failed before the fix. * The release image built from `services/` runs as uid 1000 on a read-only root filesystem, published to the broker, and logged final counters on SIGTERM. * `helm lint --strict` and `helm template` with default and override values pass and render a ServiceAccount and a StatefulSet. * `Validate-RustCrateRegistration.ps1` passes (18 crates). Grype finds no vulnerabilities in the new `Cargo.lock`. gitleaks finds no leaks. * markdownlint, markdown-table-formatter, cspell, and markdown-link-check pass on the changed Markdown. ## Validation Steps 1. `cd src/500-application/507-ai-inference/services/snapshot-normalizer && cargo test && cargo clippy --all-targets -- -D warnings` 2. `helm template t src/500-application/507-ai-inference/charts/snapshot-normalizer` and confirm a `StatefulSet` with `replicas: 1`, `AIO_MQTT_CLIENT_ID` using `$(POD_NAME)`, and `AIO_MQTT_SESSION_EXPIRY: "3600"`. 3. Optionally run a local MQTTv5 broker and publish a JPEG with an `id` user property to `INPUT_TOPIC`, as described in the crate README. ## Checklist - [x] I have updated the documentation accordingly - [x] I have added tests to cover my changes - [x] All new and existing tests passed - [ ] I have run `terraform fmt` on all Terraform code - [ ] I have run `terraform validate` on all Terraform code - [ ] I have run `az bicep format` on all Bicep code - [ ] I have run `az bicep build` to validate all Bicep code - [x] I have checked for any sensitive data/tokens that should not be committed - [x] Lint checks pass (run applicable linters for changed file types) ## Security Review - [x] No credentials, secrets, or tokens are hardcoded or logged - [x] RBAC and identity changes follow least-privilege principles - [x] No new network exposure or public endpoints introduced without justification - [x] Dependency additions or updates have been reviewed for known vulnerabilities - [x] Container image changes use pinned digests or SHA references ## Additional Notes * No image is published. The chart uses `your-registry.azurecr.io/snapshot-normalizer` as a placeholder. * Microsoft Learn doesn't document whether the media connector sets a CloudEvents `id` on `snapshot-to-mqtt` messages. Without one, messages pass through un-deduplicated and are counted as `unkeyed`. * Deduplication state is in memory, which is why the chart runs a single-replica StatefulSet. Scaling to shared subscriptions would need shared dedup state, per the #852 guidance. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Bill Berry <WilliamBerryiii@users.noreply.github.com>
1 parent cc89446 commit ce99c3a

18 files changed

Lines changed: 3743 additions & 0 deletions

File tree

‎.cspell/project-specific.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ RUSTSEC
1616
SARIF
1717
Standardised
1818
TMDL
19+
unkeyed
1920
WIQL
2021
Workback
2122
XMLA

‎.github/workflows/rust-tests.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ jobs:
3535
crate: src/500-application/507-ai-inference/services/ai-edge-inference
3636
- name: 507-ai-edge-inference-crate
3737
crate: src/500-application/507-ai-inference/services/ai-edge-inference-crate
38+
- name: 507-snapshot-normalizer
39+
crate: src/500-application/507-ai-inference/services/snapshot-normalizer
3840
- name: 507-snapshot-normalizer-core
3941
crate: src/500-application/507-ai-inference/services/snapshot-normalizer-core
4042
- name: 901-video-to-gif
@@ -200,6 +202,7 @@ jobs:
200202
- name: 504-mqtt-otel-trace-exporter
201203
- name: 507-ai-edge-inference
202204
- name: 507-ai-edge-inference-crate
205+
- name: 507-snapshot-normalizer
203206
- name: 507-snapshot-normalizer-core
204207
- name: 901-video-to-gif
205208
steps:

‎src/500-application/507-ai-inference/README.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,11 @@ This component implements a scalable AI inference service designed for industria
6565
├── services/ # Service implementations
6666
│ ├── ai-edge-inference/ # Main inference service (Rust)
6767
│ ├── ai-edge-inference-crate/ # Shared Rust crate
68+
│ ├── snapshot-normalizer/ # MQTT snapshot adapter (Rust)
6869
│ └── snapshot-normalizer-core/ # Snapshot normalization library (Rust)
6970
├── charts/ # Kubernetes deployment manifests
7071
│ ├── base/ # Base Kubernetes resources
72+
│ ├── snapshot-normalizer/ # Helm chart for the snapshot adapter
7173
│ └── model-downloader-job.yaml
7274
└── resources/ # Configuration and model files
7375
├── model_configs/ # Model configuration files
@@ -130,6 +132,24 @@ defaults, not schema-enforced limits.
130132
See [`services/snapshot-normalizer-core/README.md`](services/snapshot-normalizer-core/README.md)
131133
for the full public surface.
132134

135+
## Snapshot Normalizer
136+
137+
`services/snapshot-normalizer/` is the MQTT adapter built on the core library.
138+
It subscribes to a binary JPEG snapshot topic, such as a media connector
139+
`snapshot-to-mqtt` stream, and publishes each accepted snapshot as an
140+
`image_snapshot` v1 request to
141+
`edge-ai/v1/snapshot-normalizer/camera/{camera-id}/snapshots`, which the
142+
inference service receives through its pinned `edge-ai/v1/+/camera/+/snapshots`
143+
input filter.
144+
145+
- Carries CloudEvents attributes as MQTTv5 user properties
146+
- Deduplicates on the producer-supplied CloudEvents `id`, never on content
147+
- Refuses to start when its output topic matches its own input filter
148+
- Deploys with the [`charts/snapshot-normalizer`](charts/snapshot-normalizer/) Helm chart as a single-replica StatefulSet per camera, with a stable, unique client ID
149+
150+
See [`services/snapshot-normalizer/README.md`](services/snapshot-normalizer/README.md)
151+
for configuration, delivery semantics, and an authorization example.
152+
133153
## Quick Start
134154

135155
### Local Development
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
apiVersion: v2
2+
name: snapshot-normalizer
3+
description: Publishes binary JPEG MQTT snapshots as image_snapshot v1 requests for edge inference
4+
type: application
5+
version: 0.1.0
6+
appVersion: "0.1.0"
7+
home: https://github.com/microsoft/edge-ai
8+
sources:
9+
- https://github.com/microsoft/edge-ai
10+
maintainers:
11+
- name: Edge AI Team
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{{- define "snapshot-normalizer.name" -}}
2+
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
3+
{{- end }}
4+
5+
{{- define "snapshot-normalizer.fullname" -}}
6+
{{- if .Values.fullnameOverride }}
7+
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
8+
{{- else }}
9+
{{- $name := default .Chart.Name .Values.nameOverride }}
10+
{{- if contains $name .Release.Name }}
11+
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
12+
{{- else }}
13+
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
14+
{{- end }}
15+
{{- end }}
16+
{{- end }}
17+
18+
{{- define "snapshot-normalizer.chart" -}}
19+
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
20+
{{- end }}
21+
22+
{{- define "snapshot-normalizer.labels" -}}
23+
helm.sh/chart: {{ include "snapshot-normalizer.chart" . }}
24+
{{ include "snapshot-normalizer.selectorLabels" . }}
25+
{{- if .Chart.AppVersion }}
26+
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
27+
{{- end }}
28+
app.kubernetes.io/managed-by: {{ .Release.Service }}
29+
{{- end }}
30+
31+
{{- define "snapshot-normalizer.selectorLabels" -}}
32+
app.kubernetes.io/name: {{ include "snapshot-normalizer.name" . }}
33+
app.kubernetes.io/instance: {{ .Release.Name }}
34+
{{- end }}
35+
36+
{{- define "snapshot-normalizer.namespace" -}}
37+
{{- default "azure-iot-operations" .Values.namespace }}
38+
{{- end }}
39+
40+
{{- define "snapshot-normalizer.serviceAccountName" -}}
41+
{{- if .Values.serviceAccount.create }}
42+
{{- default (include "snapshot-normalizer.fullname" .) .Values.serviceAccount.name }}
43+
{{- else }}
44+
{{- required "serviceAccount.name is required when serviceAccount.create is false" .Values.serviceAccount.name }}
45+
{{- end }}
46+
{{- end }}
47+
48+
{{- define "snapshot-normalizer.image" -}}
49+
{{- $repository := required "image.repository is required" .Values.image.repository }}
50+
{{- printf "%s:%s" $repository (.Values.image.tag | default .Chart.AppVersion) }}
51+
{{- end }}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{{- if .Values.serviceAccount.create }}
2+
apiVersion: v1
3+
kind: ServiceAccount
4+
metadata:
5+
name: {{ include "snapshot-normalizer.serviceAccountName" . }}
6+
namespace: {{ include "snapshot-normalizer.namespace" . }}
7+
labels:
8+
{{- include "snapshot-normalizer.labels" . | nindent 4 }}
9+
{{- with .Values.serviceAccount.annotations }}
10+
annotations:
11+
{{- toYaml . | nindent 4 }}
12+
{{- end }}
13+
automountServiceAccountToken: false
14+
{{- end }}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
{{- $n := .Values.normalizer }}
2+
apiVersion: apps/v1
3+
kind: StatefulSet
4+
metadata:
5+
name: {{ include "snapshot-normalizer.fullname" . }}
6+
namespace: {{ include "snapshot-normalizer.namespace" . }}
7+
labels:
8+
{{- include "snapshot-normalizer.labels" . | nindent 4 }}
9+
spec:
10+
# In-memory deduplication and a non-shared subscription require one replica.
11+
# The StatefulSet keeps the pod name, and so the MQTT client ID, stable at
12+
# <fullname>-0 and never runs two copies, so the persistent session resumes.
13+
replicas: 1
14+
serviceName: {{ include "snapshot-normalizer.fullname" . }}
15+
updateStrategy:
16+
type: RollingUpdate
17+
selector:
18+
matchLabels:
19+
{{- include "snapshot-normalizer.selectorLabels" . | nindent 6 }}
20+
template:
21+
metadata:
22+
{{- with .Values.podAnnotations }}
23+
annotations:
24+
{{- toYaml . | nindent 8 }}
25+
{{- end }}
26+
labels:
27+
{{- include "snapshot-normalizer.selectorLabels" . | nindent 8 }}
28+
{{- with .Values.podLabels }}
29+
{{- toYaml . | nindent 8 }}
30+
{{- end }}
31+
spec:
32+
{{- with .Values.imagePullSecrets }}
33+
imagePullSecrets:
34+
{{- toYaml . | nindent 8 }}
35+
{{- end }}
36+
serviceAccountName: {{ include "snapshot-normalizer.serviceAccountName" . }}
37+
automountServiceAccountToken: false
38+
securityContext:
39+
{{- toYaml .Values.podSecurityContext | nindent 8 }}
40+
containers:
41+
- name: {{ .Chart.Name }}
42+
securityContext:
43+
{{- toYaml .Values.securityContext | nindent 12 }}
44+
image: {{ include "snapshot-normalizer.image" . }}
45+
imagePullPolicy: {{ .Values.image.pullPolicy }}
46+
env:
47+
- name: POD_NAME
48+
valueFrom:
49+
fieldRef:
50+
fieldPath: metadata.name
51+
- name: AIO_MQTT_CLIENT_ID
52+
value: {{ printf "%s-$(POD_NAME)" (required "mqtt.clientIdPrefix is required" .Values.mqtt.clientIdPrefix) | quote }}
53+
- name: AIO_BROKER_HOSTNAME
54+
value: {{ .Values.mqtt.brokerHostname | quote }}
55+
- name: AIO_BROKER_TCP_PORT
56+
value: {{ .Values.mqtt.brokerTcpPort | quote }}
57+
- name: AIO_MQTT_USE_TLS
58+
value: "true"
59+
- name: AIO_MQTT_SESSION_EXPIRY
60+
value: {{ required "mqtt.sessionExpirySeconds is required" .Values.mqtt.sessionExpirySeconds | int64 | quote }}
61+
- name: AIO_TLS_CA_FILE
62+
value: {{ .Values.mqtt.tlsCaFile | quote }}
63+
- name: AIO_SAT_FILE
64+
value: {{ .Values.mqtt.satFile | quote }}
65+
- name: CAMERA_ID
66+
value: {{ required "normalizer.cameraId is required" $n.cameraId | quote }}
67+
- name: DEVICE_NAME
68+
value: {{ required "normalizer.deviceName is required" $n.deviceName | quote }}
69+
- name: INPUT_TOPIC
70+
value: {{ required "normalizer.inputTopic is required" $n.inputTopic | quote }}
71+
{{- with $n.outputTopic }}
72+
- name: OUTPUT_TOPIC
73+
value: {{ . | quote }}
74+
{{- end }}
75+
- name: EVENT_SOURCE
76+
value: {{ $n.eventSource | quote }}
77+
{{- with $n.dataSchema }}
78+
- name: DATA_SCHEMA
79+
value: {{ . | quote }}
80+
{{- end }}
81+
- name: MAX_JPEG_BYTES
82+
value: {{ $n.maxJpegBytes | int64 | quote }}
83+
- name: MAX_ENVELOPE_BYTES
84+
value: {{ $n.maxEnvelopeBytes | int64 | quote }}
85+
- name: DEDUP_CAPACITY
86+
value: {{ $n.dedupCapacity | int64 | quote }}
87+
- name: DEDUP_WINDOW_SECONDS
88+
value: {{ $n.dedupWindowSeconds | int64 | quote }}
89+
- name: PUBLISH_ATTEMPTS
90+
value: {{ $n.publishAttempts | int64 | quote }}
91+
- name: COUNTERS_INTERVAL_SECONDS
92+
value: {{ $n.countersIntervalSeconds | int64 | quote }}
93+
- name: RUST_LOG
94+
value: {{ $n.logLevel | quote }}
95+
volumeMounts:
96+
- name: mq-sat
97+
mountPath: {{ dir .Values.mqtt.satFile }}
98+
readOnly: true
99+
- name: trust-bundle
100+
mountPath: {{ dir .Values.mqtt.tlsCaFile }}
101+
readOnly: true
102+
resources:
103+
{{- toYaml .Values.resources | nindent 12 }}
104+
volumes:
105+
- name: mq-sat
106+
projected:
107+
sources:
108+
- serviceAccountToken:
109+
path: {{ base .Values.mqtt.satFile }}
110+
audience: {{ .Values.volumes.mqSat.audience | quote }}
111+
expirationSeconds: {{ .Values.volumes.mqSat.expirationSeconds | int64 }}
112+
- name: trust-bundle
113+
configMap:
114+
name: {{ .Values.volumes.trustBundle.configMapName }}
115+
{{- with .Values.nodeSelector }}
116+
nodeSelector:
117+
{{- toYaml . | nindent 8 }}
118+
{{- end }}
119+
{{- with .Values.affinity }}
120+
affinity:
121+
{{- toYaml . | nindent 8 }}
122+
{{- end }}
123+
{{- with .Values.tolerations }}
124+
tolerations:
125+
{{- toYaml . | nindent 8 }}
126+
{{- end }}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# The adapter keeps deduplication state in memory and subscribes without a
2+
# shared subscription, so the chart always runs a single-replica StatefulSet.
3+
# Run one release per input topic to scale out.
4+
5+
image:
6+
# Placeholder. Build the image from services/snapshot-normalizer and push it
7+
# to a registry you own.
8+
repository: your-registry.azurecr.io/snapshot-normalizer
9+
pullPolicy: IfNotPresent
10+
tag: ""
11+
12+
imagePullSecrets: []
13+
nameOverride: ""
14+
fullnameOverride: ""
15+
namespace: azure-iot-operations
16+
17+
serviceAccount:
18+
# Use a distinct service account per workload trust boundary so broker
19+
# authorization rules can scope this workload alone.
20+
create: true
21+
name: ""
22+
# Annotations prefixed with aio-broker-auth/ become broker authorization
23+
# attributes for clients that authenticate with this service account token.
24+
annotations:
25+
aio-broker-auth/workload: snapshot-normalizer
26+
27+
normalizer:
28+
# Opaque, lowercase camera identifier. It becomes the subject attribute and a
29+
# segment of the default output topic.
30+
cameraId: camera-01
31+
deviceName: camera-device-01
32+
# Topic the media connector snapshot-to-mqtt stream publishes to.
33+
inputTopic: media/v1/media-connector/camera-01/snapshots
34+
# Leave empty for edge-ai/v1/snapshot-normalizer/camera/<cameraId>/snapshots,
35+
# which the component 507 inference service receives through its pinned
36+
# edge-ai/v1/+/camera/+/snapshots input filter.
37+
outputTopic: ""
38+
eventSource: snapshot-normalizer
39+
# Optional dataschema reference. Keep it consistent with any data flow source
40+
# schema that consumes the output topic.
41+
dataSchema: ""
42+
# Keep maxEnvelopeBytes below the broker maximum packet size, which the AIO
43+
# memory profile bounds (4 MB on Tiny). Base64 adds about one third.
44+
maxJpegBytes: 4194304
45+
maxEnvelopeBytes: 8388608
46+
dedupCapacity: 1024
47+
dedupWindowSeconds: 300
48+
publishAttempts: 3
49+
countersIntervalSeconds: 60
50+
logLevel: info
51+
52+
mqtt:
53+
brokerHostname: aio-broker.azure-iot-operations
54+
brokerTcpPort: "18883"
55+
# The pod connects as <clientIdPrefix>-<release fullname>-0, which is stable
56+
# across restarts so the broker resumes the persistent session.
57+
clientIdPrefix: snapshot-normalizer
58+
# Seconds the broker keeps the session, and its queued QoS 1 snapshots, after
59+
# the pod disconnects. Bounds orphaned sessions; 0 discards them at once.
60+
sessionExpirySeconds: 3600
61+
tlsCaFile: /var/run/certs/ca.crt
62+
satFile: /var/run/secrets/tokens/mq-sat
63+
64+
volumes:
65+
mqSat:
66+
audience: aio-internal
67+
expirationSeconds: 86400
68+
trustBundle:
69+
configMapName: azure-iot-operations-aio-ca-trust-bundle
70+
71+
resources:
72+
limits:
73+
cpu: 500m
74+
memory: 256Mi
75+
requests:
76+
cpu: 50m
77+
memory: 64Mi
78+
79+
podSecurityContext:
80+
runAsNonRoot: true
81+
runAsUser: 1000
82+
runAsGroup: 1000
83+
fsGroup: 1000
84+
seccompProfile:
85+
type: RuntimeDefault
86+
87+
securityContext:
88+
allowPrivilegeEscalation: false
89+
capabilities:
90+
drop:
91+
- ALL
92+
readOnlyRootFilesystem: true
93+
94+
nodeSelector: {}
95+
tolerations: []
96+
affinity: {}
97+
podAnnotations: {}
98+
podLabels: {}

‎src/500-application/507-ai-inference/docker-compose.yaml‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,23 @@ services:
4040
networks:
4141
- edge-ai-inference
4242

43+
# Snapshot normalizer: republishes binary JPEG snapshots as image_snapshot v1
44+
# requests on edge-ai/v1/snapshot-normalizer/camera/{CAMERA_ID}/snapshots,
45+
# which the inference service receives through edge-ai/v1/+/camera/+/snapshots
46+
snapshot-normalizer:
47+
build:
48+
context: ./services
49+
dockerfile: snapshot-normalizer/Dockerfile
50+
image: ${REGISTRY:-local}/${APPLICATION:-ai-inference}/snapshot-normalizer:${BUILD_ID:-local}
51+
# Defaults from the template; an optional .env beside it overrides them
52+
env_file:
53+
- path: ./services/snapshot-normalizer/.env.template
54+
- path: ./services/snapshot-normalizer/.env
55+
required: false
56+
read_only: true
57+
networks:
58+
- edge-ai-inference
59+
4360
# Optional: MQTT client for testing
4461
mqtt-client:
4562
image: eclipse-mosquitto:latest

0 commit comments

Comments
 (0)