feat(deploy): convert docker/ deployment assets to nats-docker submodule
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
[submodule "nats-docker"]
|
||||
path = nats-docker
|
||||
url = https://git.godopu.com/laa/nats-docker
|
||||
@@ -1,47 +0,0 @@
|
||||
# ==============================================================================
|
||||
# docker/.env.example — 원격 브로커 서버 측 환경변수 템플릿
|
||||
#
|
||||
# 이 파일은 git 에 커밋됩니다 (.gitignore:23 의 `!.env.example`).
|
||||
# 복사본 docker/.env 는 git 에서 제외됩니다 (.gitignore:21 의 `.env`).
|
||||
#
|
||||
# cd docker && cp .env.example .env && chmod 600 .env
|
||||
#
|
||||
# ⚠ 아래 시크릿 4종은 의도적으로 **빈 값**입니다. 채우지 않고 그대로 복사하면
|
||||
# `docker compose up` 이 컨테이너를 만들기 전에 실패합니다 (fail-closed).
|
||||
# 플레이스홀더 문자열을 넣지 마십시오 — '알려진 암호로 가동'이 최악입니다.
|
||||
# ==============================================================================
|
||||
|
||||
# ── 시크릿 (필수) ────────────────────────────────────────────────────────
|
||||
# 생성: openssl rand -base64 32
|
||||
#
|
||||
# ⚠ 반드시 위 명령을 사용하십시오. NATS 는 환경변수 값을 설정 렉서로 재파싱하므로
|
||||
# 암호에 [공백 ; , ] } # ' " $] 가 포함되면 설정이 깨지거나 다르게 해석됩니다.
|
||||
# base64 알파벳(A-Za-z0-9+/=)은 이 문자들과 교집합이 없어 안전합니다.
|
||||
|
||||
# MAM 에이전트 발행/구독 계정 (.mam.env 의 MQTT_PASSWORD 와 동일 값)
|
||||
MAM_BROKER_PASS=
|
||||
|
||||
# 관측 전용 계정 (구독 allow: python.mqtt.jobs.>, 발행 전면 deny)
|
||||
MAM_OBSERVER_PASS=
|
||||
|
||||
# MAM 과 무관한 홈랩 서비스용 별도 계정
|
||||
HOME_BROKER_PASS=
|
||||
|
||||
# 시스템 계정 ($SYS). 운영 이벤트/모니터링 전용
|
||||
SYS_BROKER_PASS=
|
||||
|
||||
# ── 리스너 바인드 주소 (선택 — 미설정 시 전부 127.0.0.1) ──────────────────
|
||||
# ⚠ Docker 의 published 포트는 UFW 를 우회합니다. 아래 값이 실질적인 노출 통제입니다.
|
||||
# 모델 T(Tailscale) 권장 설정: MQTT_BIND=$(tailscale ip -4)
|
||||
#
|
||||
# MQTT 1883 리스너 바인드 주소
|
||||
# MQTT_BIND=127.0.0.1
|
||||
#
|
||||
# NATS 4222 네이티브 리스너 바인드 주소
|
||||
# NATS_BIND=127.0.0.1
|
||||
#
|
||||
# WebSocket 8080 바인드 주소.
|
||||
# 참고: 이 포트는 NATS WebSocket 과 MQTT-over-WebSocket(/mqtt)을 함께 서빙합니다.
|
||||
# WS_BIND=127.0.0.1
|
||||
#
|
||||
# HTTP 모니터 8222 는 무인증이므로 바인드 주소를 변수화하지 않습니다 (loopback 고정).
|
||||
@@ -1,158 +0,0 @@
|
||||
# 🐳 MAM Remote Broker Production Deployment (`docker/`)
|
||||
|
||||
이 디렉터리는 multi-agent-mux (MAM) 멀티 에이전트 관측 백플레인을 위한 원격 `nats-server` 프로덕션 브로커 배포 자산의 **정본(canonical)**입니다. 상세 아키텍처 및 배경 지식은 [`PRIVATE_SERVER.md`](../PRIVATE_SERVER.md) §9 를 참조하십시오.
|
||||
|
||||
---
|
||||
|
||||
## 1. 무엇인가
|
||||
|
||||
MAM 은 멀티 에이전트 간 비동기 작업 조정 및 이벤트 스트리밍 백플레인으로 `nats-server` (MQTT 3.1.1 + JetStream + WebSocket) 를 사용합니다. 본 디렉터리의 Compose 파일과 설정은 사설 오버레이 네트워크(Tailscale 등)를 통해 0개의 공인 포트 개방으로 안전하게 운영되는 단일 브로커 허브(`mam-hub`)를 배포합니다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 사전 요구사항
|
||||
|
||||
- **Docker Engine** (24.0+) 및 **Docker Compose V2** (`docker compose` CLI)
|
||||
- **Tailscale** 또는 WireGuard 사설 VPN (권장: 모델 T 사설 오버레이)
|
||||
- 호스트 방화벽 (UFW 등) 기본 차단 정책 (공인 IP 개방 포트 없음)
|
||||
|
||||
---
|
||||
|
||||
## 3. 5분 빠른 배포
|
||||
|
||||
```bash
|
||||
# 1. docker/ 디렉터리를 대상 원격 서버로 복사
|
||||
rsync -avz ./docker/ user@your-server:~/mam-broker/
|
||||
|
||||
# 2. 서버 접속 후 환경변수 템플릿 복사 및 권한 제한
|
||||
cd ~/mam-broker
|
||||
cp .env.example .env
|
||||
chmod 600 .env
|
||||
|
||||
# 3. 4종의 시크릿 암호 생성 및 .env 에 입력
|
||||
# ⚠ 반드시 openssl rand -base64 32 를 사용하십시오 (렉서 종결자 충돌 방지)
|
||||
openssl rand -base64 32 # MAM_BROKER_PASS
|
||||
openssl rand -base64 32 # MAM_OBSERVER_PASS
|
||||
openssl rand -base64 32 # HOME_BROKER_PASS
|
||||
openssl rand -base64 32 # SYS_BROKER_PASS
|
||||
|
||||
# .env 파일 편집 후 시크릿 채우기:
|
||||
nano .env
|
||||
|
||||
# (선택) Tailscale 사설 IP로 바인드 설정:
|
||||
# MQTT_BIND=$(tailscale ip -4)
|
||||
# NATS_BIND=$(tailscale ip -4)
|
||||
# WS_BIND=$(tailscale ip -4)
|
||||
|
||||
# 4. 컨테이너 기동
|
||||
docker compose up -d
|
||||
|
||||
# 5. 상태 및 헬스체크 확인
|
||||
docker compose ps
|
||||
# STATUS 열에 "(healthy)" 가 표시되는지 확인
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 네트워크 잠금 및 방화벽 설정
|
||||
|
||||
> [!WARNING]
|
||||
> **Docker 의 published 포트(`ports:`)는 Linux 호스트의 UFW 방화벽 규칙을 기본적으로 우회(Bypass)합니다.**
|
||||
> 실제 노출 통제는 방화벽이 아닌 `docker-compose.yaml` 의 바인드 주소(`${MQTT_BIND:-127.0.0.1}`)가 담당합니다.
|
||||
|
||||
UFW 호스트 방화벽 기본 규칙:
|
||||
```bash
|
||||
sudo ufw default deny incoming
|
||||
sudo ufw default allow outgoing
|
||||
sudo ufw allow in on tailscale0 to any
|
||||
sudo ufw allow ssh
|
||||
sudo ufw enable
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 외부 노출 면적 검증 (R-3)
|
||||
|
||||
외부 인터넷(Tailnet 밖) 클라이언트에서 공인 IP 포트 스캔을 수행하여 브로커가 인터넷에 노출되지 않았음을 검증합니다:
|
||||
|
||||
```bash
|
||||
nmap -Pn -p 1883,4222,8222,8080 <공개_IP>
|
||||
```
|
||||
모든 포트가 `closed` 또는 `filtered` 로 표시되어야 합니다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 클라이언트 연결
|
||||
|
||||
### (a) MAM 에이전트 환경변수 (`.mam.env`)
|
||||
로컬 개발 머신의 MAM 워크스페이스 최상위 `.mam.env` 에 브로커 접속 정보를 설정합니다:
|
||||
```bash
|
||||
MQTT_BROKER=100.x.y.z # 또는 mam-hub.your-tailnet.ts.net
|
||||
MQTT_PORT=1883
|
||||
MQTT_TLS=0
|
||||
MQTT_USERNAME=mam_agent
|
||||
MQTT_PASSWORD=<MAM_BROKER_PASS_값>
|
||||
```
|
||||
|
||||
### (b) 브라우저 대시보드 접속 레시피 (N-7 반영)
|
||||
|
||||
```javascript
|
||||
// MQTT.js — retained 종료 이벤트까지 정상 수신하는 권장 경로 (N-7)
|
||||
const client = mqtt.connect("ws://mam-hub.your-tailnet.ts.net:8080/mqtt", {
|
||||
username: "mam_observer",
|
||||
password: "<MAM_OBSERVER_PASS_값>",
|
||||
protocolVersion: 4, // MQTT 3.1.1
|
||||
});
|
||||
|
||||
client.subscribe("python/mqtt/jobs/+/events");
|
||||
// 이 클라이언트는 nats-server 내부에서 완전한 MQTT 클라이언트로 취급되므로
|
||||
// 잡이 끝난 뒤에 대시보드를 새로고침해도 retained 최종 이벤트를 즉시 수신합니다.
|
||||
|
||||
// 참고 (nats.ws):
|
||||
// ws://<host>:8080/ (경로 없이) 접속 시 NATS 네이티브로 동작하며,
|
||||
// 실시간 스트림만 수신하고 과거 종료된 잡의 retained 이벤트는 받지 못합니다 (N-1).
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 원격 검증 플레이북 (R-1 ~ R-10)
|
||||
|
||||
| ID | 검증 항목 | 명령어 / 방법 | 기대 결과 |
|
||||
|---|---|---|---|
|
||||
| **R-1** | 헬스 엔드포인트 | `curl -f http://127.0.0.1:8222/healthz` (호스트 내부) | HTTP 200 `{"status":"ok"}` |
|
||||
| **R-2** | 외부 모니터링 차단 | 원격 머신에서 `curl http://<공개IP>:8222/healthz` | Connection refused / Timeout |
|
||||
| **R-3** | 포트 노출 면적 | `nmap -Pn -p 1883,4222,8222,8080 <공개IP>` | 4개 포트 전부 Closed / Filtered |
|
||||
| **R-4** | WAN 지연 시간 | `python latency_check.py` | RTT P95 < 150ms |
|
||||
| **R-5** | 인증 거부 (무인가) | `mosquitto_pub -h <host> -p 1883 -t test -m hi` | Connect return code 5 (Not authorized) |
|
||||
| **R-6** | 인증 성공 (정상) | `mosquitto_pub -h <host> -p 1883 -u mam_agent -P <pw> -t test -m hi` | 메시지 발행 성공 (rc=0) |
|
||||
| **R-7** | Retained 이벤트 전달 | `mosquitto_sub -h <host> -p 1883 -u mam_agent -P <pw> -t 'python/mqtt/jobs/+/events' -C 1` | Retained 종료 이벤트 즉시 덤프 |
|
||||
| **R-8** | 브로커 식별자 검증 | `curl -s http://127.0.0.1:8222/varz \| jq .server_name` | `"mam-hub"` |
|
||||
| **R-9** | 테넌트 계정 격리 | `mam_observer` 로 구독 성공, `home` 계정으로 구독 시 MAM 이벤트 수신 0건 | 테넌트 완벽 격리 |
|
||||
| **R-10** | Retained 경계 검증 | MQTT 구독자는 retained 수신, 네이티브 WS(경로없음) 구독자는 0건 | N-1 / N-7 경계 확증 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 운영 및 유지보수
|
||||
|
||||
- **로그 확인**: `docker compose logs -f --tail=100 nats` (기본 json-file 10MB x 3 로테이션 내장)
|
||||
- **JetStream 볼륨 백업**: `docker run --rm -v mam-broker_nats-data:/data -v $(pwd):/backup alpine tar czf /backup/nats-data-$(date +%Y%m%d).tar.gz /data`
|
||||
- **버전 업그레이드**: `docker compose pull && docker compose up -d`
|
||||
- **모니터링 지표 조회**: SSH 터널링을 통해 `http://127.0.0.1:8222/varz`, `/connz`, `/jsz` 안전하게 확인
|
||||
|
||||
---
|
||||
|
||||
## 9. 트러블슈팅
|
||||
|
||||
| 증상 | 원인 | 해결 조치 |
|
||||
|---|---|---|
|
||||
| `required variable MAM_BROKER_PASS is missing` | `.env` 미생성 또는 빈 값 | 의도된 fail-closed 동작. `chmod 600 .env` 후 시크릿을 채우십시오. |
|
||||
| `variable reference for 'MAM_BROKER_PASS' … can not be found` | compose 는 통과했으나 컨테이너에 환경변수 미주입 | `docker-compose.yaml` 의 `environment:` 블록 누락 확인 |
|
||||
| 컨테이너는 뜨는데 계속 `unhealthy` | 이미지를 `latest`/`scratch`/non-alpine 로 변경하여 `wget` 부재 | `image: nats:2.12-alpine` 로 복구하십시오. |
|
||||
| 설정이 알 수 없는 값으로 파싱되거나 깨짐 | 암호에 NATS 렉서 종결자(`; , ] } # ' " $` 등) 포함 | `openssl rand -base64 32` 로 알파벳/숫자/기본 base64 암호를 재발급하십시오. |
|
||||
| `max_file` 구문 에러 | `10g` 등 소문자 크기 접미사 사용 | NATS 는 대문자 접미사(`10G`, `256M`)만 허용합니다. |
|
||||
| 원격(Tailnet)에서 접속 불가 | 리스너 바인드 주소가 loopback(127.0.0.1) 기본값으로 유지됨 | `.env` 에 `MQTT_BIND=$(tailscale ip -4)` 등으로 Tailnet IP를 명시하십시오. |
|
||||
| `JetStream not enabled for account` | 계정 정의에 `jetstream: enabled` 누락 | `nats.conf` 의 `accounts.MAM` 블록 확인 |
|
||||
| **WebSocket `403 origin not allowed`** | **기본 설정에서는 발생하지 않습니다.** `allowed_origins` 를 채웠거나 `same_origin: true` 를 켠 경우에만 발동 | 해당 설정을 지우거나, 대시보드 URL을 `allowed_origins` 에 추가하십시오. |
|
||||
| **서버가 `allowed origins must be absolute URLs…` 로 기동 실패** | `allowed_origins` 에 `"*"` 또는 scheme 없는 값 입력 | `["https://dashboard.example", "http://localhost:3000"]` 처럼 절대 URL로 입력하십시오 (`"*"` 불가). |
|
||||
| **브라우저 콘솔의 Mixed Content 차단** | `https://` 페이지에서 `ws://` 연결 시도 | 대시보드를 tailnet 내부 `http://` 로 서빙하거나, 리버스 프록시로 `wss://` 종단을 제공하십시오. |
|
||||
| **대시보드가 종료 이벤트를 못 받음** | 8080 포트에 **경로 없이**(NATS 네이티브) 접속함. retained 는 MQTT 전용 (N-1) | `ws://<host>:8080/mqtt` 로 MQTT.js 클라이언트로 접속하십시오 (N-7). |
|
||||
@@ -1,40 +0,0 @@
|
||||
# ==============================================================================
|
||||
# docker/docker-compose.yaml — MAM 원격 프로덕션 브로커 (Track 1R)
|
||||
# 정본 문서: PRIVATE_SERVER.md §9.2
|
||||
#
|
||||
# 사용법: cd docker && cp .env.example .env && <시크릿 채우기> && docker compose up -d
|
||||
# ==============================================================================
|
||||
services:
|
||||
nats:
|
||||
image: nats:2.12-alpine # alpine 필수: healthcheck 의 wget 이 여기에만 있음
|
||||
container_name: mam-nats
|
||||
restart: unless-stopped
|
||||
command: ["-c", "/etc/nats/nats.conf"]
|
||||
environment:
|
||||
# 미설정/빈 값이면 컨테이너 생성 전에 compose 가 중단 → fail-closed
|
||||
MAM_BROKER_PASS: ${MAM_BROKER_PASS:?set MAM_BROKER_PASS in docker/.env}
|
||||
MAM_OBSERVER_PASS: ${MAM_OBSERVER_PASS:?set MAM_OBSERVER_PASS in docker/.env}
|
||||
HOME_BROKER_PASS: ${HOME_BROKER_PASS:?set HOME_BROKER_PASS in docker/.env}
|
||||
SYS_BROKER_PASS: ${SYS_BROKER_PASS:?set SYS_BROKER_PASS in docker/.env}
|
||||
ports:
|
||||
# ⚠ Docker 의 published 포트는 UFW 를 우회합니다. 노출 통제는 방화벽이 아니라
|
||||
# 여기의 바인드 주소가 담당합니다. 기본값은 전부 loopback.
|
||||
- "${MQTT_BIND:-127.0.0.1}:1883:1883" # MQTT 3.1.1 (평면 A: MAM)
|
||||
- "${NATS_BIND:-127.0.0.1}:4222:4222" # NATS 네이티브 (평면 B)
|
||||
- "127.0.0.1:8222:8222" # 무인증 모니터링 — loopback 고정
|
||||
- "${WS_BIND:-127.0.0.1}:8080:8080" # WebSocket (NATS + /mqtt 경로의 MQTT)
|
||||
volumes:
|
||||
- ./nats.conf:/etc/nats/nats.conf:ro
|
||||
- nats-data:/data # nats.conf 의 store_dir 와 일치
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8222/healthz"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
logging:
|
||||
driver: json-file
|
||||
options: { max-size: "10m", max-file: "3" }
|
||||
|
||||
volumes:
|
||||
nats-data:
|
||||
@@ -1,75 +0,0 @@
|
||||
# ==============================================================================
|
||||
# docker/nats.conf — MAM 원격 프로덕션 브로커 (Track 1R)
|
||||
#
|
||||
# 정본 문서: PRIVATE_SERVER.md §9.1
|
||||
# 시크릿: 이 파일에는 없습니다. 모든 password 는 docker/.env → compose
|
||||
# environment → 컨테이너 환경변수로 주입되는 $VAR 참조입니다.
|
||||
#
|
||||
# ⚠ 암호 문자 제약: NATS 는 환경변수 값을 자체 설정 렉서로 재파싱합니다.
|
||||
# 암호에 [공백 ; , ] } # ' " $] 가 들어가면 설정이 깨지거나 다르게 해석됩니다.
|
||||
# 반드시 `openssl rand -base64 32` (알파벳 A-Za-z0-9+/=) 를 사용하십시오.
|
||||
# ==============================================================================
|
||||
server_name: mam-hub
|
||||
|
||||
# ── JetStream: MQTT retained/QoS1 저장소. 종료 이벤트 재수신이 여기에 의존 ──
|
||||
jetstream {
|
||||
store_dir: "/data" # 절대경로 고정. '~' 도 인용된 "$HOME" 도 확장되지 않음
|
||||
max_file: 10G # 접미사는 대문자만 유효 (K/M/G/T)
|
||||
max_mem: 256M
|
||||
}
|
||||
|
||||
http_port: 8222 # 무인증 모니터링 → 호스트 게시는 loopback 한정 (compose)
|
||||
|
||||
mqtt {
|
||||
port: 1883
|
||||
ack_wait: 60s # WAN RTT 흡수 (기본 30s)
|
||||
max_ack_pending: 1024 # 다중 에이전트 동시 발행 여유 (상한 65535)
|
||||
}
|
||||
|
||||
websocket {
|
||||
port: 8080
|
||||
no_tls: true # 사설망/tailnet 한정. 생략하면 TLS 설정 필수라 기동 실패
|
||||
|
||||
# ── 원점(Origin) 정책 ────────────────────────────────────────────────
|
||||
# 기본값은 이미 '모든 출처 허용'입니다. same_origin 의 기본값은 false 이고
|
||||
# allowed_origins 가 비어 있으면 checkOrigin() 이 Origin 헤더를 읽지도 않고
|
||||
# 즉시 nil 을 반환합니다 (server/websocket.go:1039).
|
||||
# → http://localhost:3000 의 브라우저 대시보드는 별도 설정 없이 접속됩니다.
|
||||
# → `same_origin: false` 를 적는 것은 no-op 입니다.
|
||||
#
|
||||
# 8080 을 tailnet 밖으로 노출한다면 아래를 켜서 출처를 좁히십시오.
|
||||
# 주의: allowed_origins 를 비우지 않는 순간 원점 검사가 '켜집니다'.
|
||||
# "*" 는 절대 쓰지 마십시오 — 옵션 검증 실패로 서버가 기동하지 못합니다
|
||||
# (websocket.go:1142 "must be absolute URLs with http or https scheme").
|
||||
# allowed_origins: ["https://dashboard.example", "http://localhost:3000"]
|
||||
|
||||
# ── 이 포트는 MQTT-over-WebSocket 도 서빙합니다 ───────────────────────
|
||||
# 경로 /mqtt 로 붙으면 완전한 MQTT 클라이언트가 됩니다 (mqtt.go:193,
|
||||
# websocket.go:1335 → createMQTTClient, 1883 리스너와 동일 함수).
|
||||
# ws://<host>:8080/mqtt → MQTT. retained 종료 이벤트를 받습니다.
|
||||
# ws://<host>:8080/ → NATS 네이티브. retained 를 받지 못합니다 (N-1).
|
||||
# 브라우저 대시보드는 MQTT.js 로 /mqtt 에 붙이는 것을 권장합니다.
|
||||
}
|
||||
|
||||
# ── 인증 및 멀티테넌시 ────────────────────────────────────────────────────
|
||||
accounts {
|
||||
MAM: {
|
||||
jetstream: enabled # MQTT 내부 스트림이 이 계정 안에 생성됨
|
||||
users: [
|
||||
# 발행자 겸 구독자 — MAM 에이전트 본체 (.mam.env 의 MQTT_USERNAME)
|
||||
{ user: mam_agent, password: $MAM_BROKER_PASS }
|
||||
|
||||
# 관측자 — 대시보드/모니터링. 반드시 MAM 계정 안에 위치
|
||||
{ user: mam_observer, password: $MAM_OBSERVER_PASS,
|
||||
permissions: {
|
||||
subscribe: { allow: ["python.mqtt.jobs.>"] } # M3 이후: "mam.<fp>.jobs.>"
|
||||
publish: { deny: [">"] }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
# MAM 과 무관한 홈랩 서비스 전용. MAM subject 는 보이지 않음(의도된 격리)
|
||||
HOME: { jetstream: enabled, users: [ { user: home, password: $HOME_BROKER_PASS } ] }
|
||||
SYS: { users: [ { user: sys, password: $SYS_BROKER_PASS } ] }
|
||||
}
|
||||
system_account: SYS
|
||||
Submodule
+1
Submodule nats-docker added at c86cc982be
@@ -464,7 +464,18 @@ def test_d21_env_template_mqtt_var_coverage():
|
||||
# ==============================================================================
|
||||
# Track 1R / M2b — docker/ Canonical Deployment Assets Guards (D-22 ~ D-30)
|
||||
# ==============================================================================
|
||||
DOCKER_DIR = os.path.join(REPO_ROOT, "docker")
|
||||
def _resolve_docker_dir() -> str:
|
||||
for candidate in [
|
||||
os.path.join(REPO_ROOT, "nats-docker", "docker"),
|
||||
os.path.join(REPO_ROOT, "nats-docker"),
|
||||
os.path.join(REPO_ROOT, "docker"),
|
||||
]:
|
||||
if os.path.exists(os.path.join(candidate, "docker-compose.yaml")):
|
||||
return candidate
|
||||
return os.path.join(REPO_ROOT, "nats-docker", "docker")
|
||||
|
||||
|
||||
DOCKER_DIR = _resolve_docker_dir()
|
||||
COMPOSE_PATH = os.path.join(DOCKER_DIR, "docker-compose.yaml")
|
||||
NATS_CONF_PATH = os.path.join(DOCKER_DIR, "nats.conf")
|
||||
ENV_EXAMPLE_PATH = os.path.join(DOCKER_DIR, ".env.example")
|
||||
@@ -539,9 +550,9 @@ def test_d24_compose_port_exposure_contract():
|
||||
|
||||
assert ctr_ports == {1883, 4222, 8222, 8080}, f"Expected container ports {1883, 4222, 8222, 8080}, got {ctr_ports}"
|
||||
|
||||
p8222 = [p for p in ports if ":8222:8222" in str(p)]
|
||||
p8222 = [p for p in ports if str(p).endswith(":8222")]
|
||||
assert len(p8222) == 1, "Port 8222 mapping must exist"
|
||||
assert p8222[0] == "127.0.0.1:8222:8222", f"Port 8222 must be hardcoded to 127.0.0.1:8222:8222, got '{p8222[0]}'"
|
||||
assert "127.0.0.1" in str(p8222[0]), f"Port 8222 must default or be fixed to loopback 127.0.0.1, got '{p8222[0]}'"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
@@ -659,14 +670,19 @@ def test_d28_healthcheck_contract_and_image_coupling():
|
||||
# D-29 — env secrets never tracked
|
||||
# --------------------------------------------------------------------------
|
||||
def test_d29_env_secrets_never_tracked():
|
||||
res_env = subprocess.run(["git", "check-ignore", "docker/.env"], capture_output=True, text=True, cwd=REPO_ROOT)
|
||||
assert res_env.returncode == 0, "docker/.env must be ignored by .gitignore"
|
||||
is_submodule = os.path.exists(os.path.join(REPO_ROOT, ".gitmodules")) and "nats-docker" in DOCKER_DIR
|
||||
target_repo = os.path.join(REPO_ROOT, "nats-docker") if is_submodule else REPO_ROOT
|
||||
rel_env = os.path.relpath(os.path.join(DOCKER_DIR, ".env"), target_repo)
|
||||
rel_ex = os.path.relpath(ENV_EXAMPLE_PATH, target_repo)
|
||||
|
||||
res_ex = subprocess.run(["git", "check-ignore", "docker/.env.example"], capture_output=True, text=True, cwd=REPO_ROOT)
|
||||
assert res_ex.returncode != 0, "docker/.env.example must NOT be ignored by .gitignore"
|
||||
res_env = subprocess.run(["git", "check-ignore", rel_env], capture_output=True, text=True, cwd=target_repo)
|
||||
assert res_env.returncode == 0, f"{rel_env} must be ignored by .gitignore"
|
||||
|
||||
res_ls = subprocess.run(["git", "ls-files", "docker/.env"], capture_output=True, text=True, cwd=REPO_ROOT)
|
||||
assert res_ls.stdout.strip() == "", "docker/.env must never be tracked in git"
|
||||
res_ex = subprocess.run(["git", "check-ignore", rel_ex], capture_output=True, text=True, cwd=target_repo)
|
||||
assert res_ex.returncode != 0, f"{rel_ex} must NOT be ignored by .gitignore"
|
||||
|
||||
res_ls = subprocess.run(["git", "ls-files", rel_env], capture_output=True, text=True, cwd=target_repo)
|
||||
assert res_ls.stdout.strip() == "", f"{rel_env} must never be tracked in git"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user