package compose import future.keywords.if import future.keywords.contains import future.keywords.in # Every service must have a restart policy deny_no_restart contains msg if { some name, svc in input.services not svc.restart msg := sprintf("service %s: missing restart policy", [name]) } # zigbee2mqtt must use the Pine64 BLZ fork, not upstream koenkk deny_wrong_image contains msg if { svc := input.services.zigbee2mqtt not startswith(svc.image, "ghcr.io/pine64/zigbee2mqtt") msg := sprintf("zigbee2mqtt: image must be ghcr.io/pine64/zigbee2mqtt (got %s)", [svc.image]) } # zigbee2mqtt must have the correct entrypoint deny_missing_entrypoint contains msg if { svc := input.services.zigbee2mqtt svc.entrypoint != ["/app/data/scripts/entrypoint.sh"] msg := "zigbee2mqtt: entrypoint must be [\"/app/data/scripts/entrypoint.sh\"]" } # zigbee2mqtt must have the correct command (tini wrapper) deny_wrong_command contains msg if { svc := input.services.zigbee2mqtt svc.command != ["/sbin/tini", "--", "node", "index.js"] msg := sprintf("zigbee2mqtt: command must be /sbin/tini -- node index.js (got %s)", [svc.command]) } # Device sources should use stable by-path or by-id symlinks, not raw /dev/ttyUSB* deny_raw_tty contains msg if { some name, svc in input.services some dev in svc.devices startswith(dev.source, "/dev/ttyUSB") msg := sprintf("service %s: device source %s uses raw tty path (use /dev/serial/by-*)", [name, dev.source]) } # Only audio-bridge should run privileged deny_unexpected_privileged contains msg if { some name, svc in input.services svc.privileged name != "audio-bridge" msg := sprintf("service %s: privileged: true (only audio-bridge should be privileged)", [name]) } # zigbee2mqtt must depend on mosquitto deny_missing_dep contains msg if { svc := input.services.zigbee2mqtt not svc.depends_on msg := "zigbee2mqtt: missing depends_on (should depend on mosquitto)" } deny_missing_dep contains msg if { svc := input.services.zigbee2mqtt svc.depends_on not "mosquitto" in object.keys(svc.depends_on) msg := "zigbee2mqtt: must depend on mosquitto" } # nodered must depend on mosquitto with condition: service_healthy deny_nodered_dep_healthy contains msg if { svc := input.services.nodered dep := svc.depends_on.mosquitto dep.condition != "service_healthy" msg := "nodered: mosquitto dependency must have condition: service_healthy" } # zigbee2mqtt should not use host networking deny_host_network contains msg if { svc := input.services.zigbee2mqtt svc.network_mode == "host" msg := "zigbee2mqtt: should not use host network mode" } # zigbee2mqtt must mount zigbee2mqtt/ as /app/data has_z2m_vol if { some vol in input.services.zigbee2mqtt.volumes startswith(vol, "./zigbee2mqtt:/app/data") } deny_missing_volume contains msg if { svc := input.services.zigbee2mqtt not has_z2m_vol msg := "zigbee2mqtt: must have volume mapping ./zigbee2mqtt:/app/data" } # Cross-reference: zigbee2mqtt device target must match its config port. # Load configuration.yaml via --data flag: # conftest test infra/compose.yaml --data infra/zigbee2mqtt/configuration.yaml deny_port_mismatch contains msg if { svc := input.services.zigbee2mqtt some dev in svc.devices cfg := data.configuration.serial.port dev.target != cfg msg := sprintf("zigbee2mqtt: device target %s does not match config port %s", [dev.target, cfg]) }