feat(database): unify recipe host ports and add Consul, Elasticsearch

This commit is contained in:
二丫讲梵
2026-08-10 18:27:20 +08:00
committed by GitHub
parent deba5db2fd
commit f5e7a37c8d
49 changed files with 522 additions and 135 deletions
+2 -2
View File
@@ -11,13 +11,13 @@ make db-verify DB=postgresql@17.4
make db-down DB=postgresql@17.4
```
Every network recipe uses the standard database port plus one on the host. The default password is `123456`, and the default database is `dbx`; Redis uses DB 0 and the `dbx:` key prefix. Container names follow `dbx-<product>-<version>`. Versions of the same database share the default host port, so use `DB_PORT` when running them concurrently. `DB_PORT` and `DB_PASSWORD` override the default host port and password. Ports bind to `127.0.0.1` by default. To allow remote access explicitly, set `DB_BIND_ADDRESS=0.0.0.0`, choose a strong `DB_PASSWORD`, and protect the host with firewall rules. `make db-reset` deletes the named volume and therefore requires `CONFIRM=1`:
Each product has a dedicated `101xx`-`115xx` default host-port range, so checked-in recipes can run together without using common service ports or colliding across versions. The default password is `123456`, and the default database is `dbx`; Redis uses DB 0 and the `dbx:` key prefix. Container names follow `dbx-<product>-<version>`. `DB_PORT` and `DB_PASSWORD` override the primary host port and password; service-specific variables override auxiliary ports. Ports bind to `127.0.0.1` by default. To allow remote access explicitly, set `DB_BIND_ADDRESS=0.0.0.0`, choose a strong `DB_PASSWORD`, and protect the host with firewall rules. `make db-reset` deletes the named volume and therefore requires `CONFIRM=1`:
```bash
make db-reset DB=redis@7.4 CONFIRM=1
```
The primary Make targets are `db-list`, `db`, `db-verify`, `db-down`, `db-reset`, and `db-check`. Run `make db` to print one copyable start command for every recipe. `make db-completion` prints Bash, Zsh, and PowerShell completion setup; the scripts in `completion/` dynamically complete recipe selectors. The targets avoid POSIX-shell conditionals and work with GNU Make in PowerShell, Git Bash, or WSL. For diagnosis, use the lower-level `pnpm db:env -- info|status|logs|shell <product> <version>` commands.
The primary Make targets are `db-list`, `db`, `db-verify`, `db-down`, `db-reset`, and `db-check`. `make db-list` groups recipe versions by product and shows each container-to-host default port mapping, image, and supported platform. Run `make db` to print one copyable start command for every recipe. For connection types supported by DBX, a completed startup also prints a prefilled `dbx://connection/new` link; on macOS with DBX Desktop installed, run `open '<link>'` to open the new-connection dialog. The link can contain a password, so do not save it in shared terminal history, logs, or tickets. Recipes with no compatible DBX connection type explicitly report that no deep link is available. `make db-completion` prints Bash, Zsh, and PowerShell completion setup; the scripts in `completion/` dynamically complete recipe selectors. The targets avoid POSIX-shell conditionals and work with GNU Make in PowerShell, Git Bash, or WSL. For diagnosis, use the lower-level `pnpm db:env -- info|status|logs|shell <product> <version>` commands.
## Recipe layout
+2 -2
View File
@@ -11,13 +11,13 @@ make db-verify DB=postgresql@17.4
make db-down DB=postgresql@17.4
```
所有网络数据库默认使用标准端口 `+1` 的宿主端口,默认密码统一为 `123456`,默认数据库名为 `dbx`;Redis 使用 DB 0 和 `dbx:` 键前缀。容器名统一为 `dbx-<product>-<version>`。同一种数据库的不同版本共用默认宿主端口,并行启动时请使用 `DB_PORT` 覆盖其中一个端口。`DB_PORT` 和 `DB_PASSWORD` 可以覆盖默认主机端口和密码。端口默认仅绑定 `127.0.0.1`;如需远程访问,必须显式设置 `DB_BIND_ADDRESS=0.0.0.0`,同时使用强密码和防火墙限制访问。`make db-reset` 会删除命名卷及其数据,因此必须显式传入 `CONFIRM=1`:
每个产品都有专属的 `101xx` 至 `115xx` 默认宿主端口段,因此所有已提交配方可同时启动,且不会占用常见服务端口或在版本间冲突。默认密码统一为 `123456`,默认数据库名为 `dbx`;Redis 使用 DB 0 和 `dbx:` 键前缀。容器名统一为 `dbx-<product>-<version>`。`DB_PORT` 和 `DB_PASSWORD` 可覆盖主连接端口和密码;辅助端口使用各服务原有的专属环境变量覆盖。端口默认仅绑定 `127.0.0.1`;如需远程访问,必须显式设置 `DB_BIND_ADDRESS=0.0.0.0`,同时使用强密码和防火墙限制访问。`make db-reset` 会删除命名卷及其数据,因此必须显式传入 `CONFIRM=1`:
```bash
make db-reset DB=redis@7.4 CONFIRM=1
```
主要 Make 目标为 `db-list`、`db`、`db-verify`、`db-down`、`db-reset` 和 `db-check`。执行 `make db` 会为每个配方输出一条可直接复制的启动命令。`make db-completion` 会输出 Bash、Zsh 和 PowerShell 补全加载命令,`completion/` 中的脚本会动态补全配方选择器。目标不再依赖 POSIX Shell 条件语法,可在 PowerShell、Git Bash 或 WSL 中通过 GNU Make 使用。诊断时可使用底层命令 `pnpm db:env -- info|status|logs|shell <product> <version>`。
主要 Make 目标为 `db-list`、`db`、`db-verify`、`db-down`、`db-reset` 和 `db-check`。`make db-list` 会按产品归并展示各版本,并列出每项默认的容器到宿主机端口映射、镜像和支持平台。执行 `make db` 会为每个配方输出一条可直接复制的启动命令;对于 DBX 已支持的连接类型,启动完成后还会输出预填的 `dbx://connection/new` 链接,可在已安装 DBX 桌面端的 macOS 上通过 `open '<链接>'` 打开新建连接窗口。该链接可能包含密码,请不要将其写入共享终端历史、日志或工单。没有对应 DBX 连接类型的配方会显示深链不可用。`make db-completion` 会输出 Bash、Zsh 和 PowerShell 补全加载命令,`completion/` 中的脚本会动态补全配方选择器。目标不再依赖 POSIX Shell 条件语法,可在 PowerShell、Git Bash 或 WSL 中通过 GNU Make 使用。诊断时可使用底层命令 `pnpm db:env -- info|status|logs|shell <product> <version>`。
## 配方结构
+2 -2
View File
@@ -1,8 +1,8 @@
# Database Recipe Template
Create `deploy/database/<product>/<version>/` with `recipe.json`, `compose.yaml`, and an `init/` directory. Set `defaultPort` in `recipe.json` to the native service port; the connection port and Compose `DB_PORT` fallback must both be `defaultPort + 1`.
Create `deploy/database/<product>/<version>/` with `recipe.json`, `compose.yaml`, and an `init/` directory. Set `defaultPort` to the native service port, then declare every default host mapping in `hostPorts`. `connection.port` and the Compose `DB_PORT` fallback must both match `hostPorts.DB_PORT`.
Every recipe must use a pinned image, default its host binding to `${DB_BIND_ADDRESS:-127.0.0.1}`, use the standard port plus one by default, set `container_name` to `dbx-<product>-<version>`, use password `123456`, and initialize database `dbx`. Redis-like services without named databases use DB 0 and the `dbx:` key prefix.
Every recipe must use a pinned image, default its host binding to `${DB_BIND_ADDRESS:-127.0.0.1}`, allocate unique host ports from its product's `101xx`-`115xx` range, set `container_name` to `dbx-<product>-<version>`, use password `123456`, and initialize database `dbx`. Redis-like services without named databases use DB 0 and the `dbx:` key prefix.
Add both a non-interactive `smoke.steps` command and an interactive `shell` command to `recipe.json`, then run:
+1 -1
View File
@@ -4,7 +4,7 @@ services:
container_name: dbx-clickhouse-24.8
restart: always
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-8124}:8123"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-10600}:8123"
environment:
CLICKHOUSE_DB: dbx
CLICKHOUSE_USER: default
+2 -1
View File
@@ -7,7 +7,8 @@
"platforms": ["linux/amd64", "linux/arm64"],
"service": "database",
"defaultPort": 8123,
"connection": { "host": "127.0.0.1", "port": 8124, "username": "default", "password": "123456", "database": "dbx" },
"connection": { "host": "127.0.0.1", "port": 10600, "username": "default", "password": "123456", "database": "dbx" },
"hostPorts": { "DB_PORT": 10600 },
"shell": ["clickhouse-client", "--user", "default", "--password", "${DB_PASSWORD}", "--database", "dbx"],
"smoke": { "steps": [{ "name": "query initialized row", "command": ["clickhouse-client", "--user", "default", "--password", "${DB_PASSWORD}", "--database", "dbx", "--query", "SELECT note FROM dbx_smoke WHERE id = 1"], "expect": "DBX smoke" }] }
}
+26
View File
@@ -0,0 +1,26 @@
services:
database:
image: docker.cnb.cool/znb/images/consul:2.0.2
container_name: dbx-consul-2.0.2
restart: unless-stopped
command:
- agent
- -dev
- -client=0.0.0.0
- -ui
- -data-dir=/consul/data
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-10900}:8500"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${CONSUL_GRPC_PORT:-10901}:8502"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${CONSUL_DNS_PORT:-10902}:8600/tcp"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${CONSUL_DNS_PORT:-10902}:8600/udp"
volumes:
- data:/consul/data
healthcheck:
test: ["CMD", "consul", "members", "-http-addr=http://127.0.0.1:8500"]
interval: 5s
timeout: 5s
retries: 30
start_period: 10s
volumes:
data:
@@ -0,0 +1,3 @@
# Consul development data
The Consul development agent starts without ACL authentication. `make db-verify DB=consul@2.0.2` writes and reads the `dbx/smoke` KV value.
+14
View File
@@ -0,0 +1,14 @@
{
"database": "consul",
"name": "Consul",
"version": "2.0.2",
"displayVersion": "2.0.2",
"image": "docker.cnb.cool/znb/images/consul:2.0.2",
"platforms": ["linux/amd64", "linux/arm64"],
"service": "database",
"defaultPort": 8500,
"connection": { "host": "127.0.0.1", "port": 10900, "authentication": "none", "database": "dbx", "grpcPort": 10901, "dnsPort": 10902 },
"hostPorts": { "DB_PORT": 10900, "CONSUL_GRPC_PORT": 10901, "CONSUL_DNS_PORT": 10902 },
"shell": ["consul", "members", "-http-addr=http://127.0.0.1:8500"],
"smoke": { "steps": [{ "name": "write a Consul KV smoke value", "command": ["consul", "kv", "put", "-http-addr=http://127.0.0.1:8500", "dbx/smoke", "DBX smoke"], "expect": "Success! Data written" }, { "name": "read the Consul KV smoke value", "command": ["consul", "kv", "get", "-http-addr=http://127.0.0.1:8500", "dbx/smoke"], "expect": "DBX smoke" }] }
}
@@ -0,0 +1,21 @@
services:
database:
image: docker.cnb.cool/znb/images/elasticsearch:6.8
container_name: dbx-elasticsearch-6.8
restart: always
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-11500}:9200"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${ELASTICSEARCH_TRANSPORT_PORT:-11501}:9300"
environment:
discovery.type: single-node
xpack.ml.enabled: "false"
volumes:
- data:/usr/share/elasticsearch/data
healthcheck:
test: ["CMD-SHELL", "curl --fail --silent 'http://127.0.0.1:9200/_cluster/health?wait_for_status=yellow&timeout=1s' >/dev/null"]
interval: 5s
timeout: 5s
retries: 30
start_period: 20s
volumes:
data:
@@ -0,0 +1,3 @@
# Elasticsearch 6.8 smoke data
`make db-verify DB=elasticsearch@6.8` indexes and reads the `dbx-smoke` document. Elasticsearch runs in single-node development mode without authentication.
@@ -0,0 +1,14 @@
{
"database": "elasticsearch",
"name": "Elasticsearch",
"version": "6.8",
"displayVersion": "6.8",
"image": "docker.cnb.cool/znb/images/elasticsearch:6.8",
"platforms": ["linux/amd64", "linux/arm64"],
"service": "database",
"defaultPort": 9200,
"connection": { "host": "127.0.0.1", "port": 11500, "authentication": "none", "database": "dbx", "transportPort": 11501 },
"hostPorts": { "DB_PORT": 11500, "ELASTICSEARCH_TRANSPORT_PORT": 11501 },
"shell": ["bash"],
"smoke": { "steps": [{ "name": "index a smoke document", "command": ["bash", "-c", "curl --fail --silent --show-error -X PUT -H 'Content-Type: application/json' -d '{\"message\":\"DBX smoke\"}' 'http://127.0.0.1:9200/dbx-smoke/_doc/1?refresh=true'"], "expect": "\"result\"" }, { "name": "read the smoke document", "command": ["bash", "-c", "curl --fail --silent --show-error http://127.0.0.1:9200/dbx-smoke/_doc/1"], "expect": "DBX smoke" }] }
}
+2 -2
View File
@@ -18,8 +18,8 @@ services:
- --logger=zap
- --log-outputs=stderr
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-2380}:2379"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${ETCD_PEER_PORT:-2381}:2380"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-10700}:2379"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${ETCD_PEER_PORT:-10701}:2380"
volumes:
- data:/etcd-data
healthcheck:
+2 -1
View File
@@ -7,7 +7,8 @@
"platforms": ["linux/amd64", "linux/arm64"],
"service": "database",
"defaultPort": 2379,
"connection": { "host": "127.0.0.1", "port": 2380, "username": "root", "password": "123456", "database": "dbx", "peerPort": 2381 },
"connection": { "host": "127.0.0.1", "port": 10700, "username": "root", "password": "123456", "database": "dbx", "peerPort": 10701 },
"hostPorts": { "DB_PORT": 10700, "ETCD_PEER_PORT": 10701 },
"bootstrap": {
"check": { "command": ["/usr/local/bin/etcdctl", "--endpoints=http://127.0.0.1:2379", "--user", "root:${DB_PASSWORD}", "auth", "status"], "expect": "Authentication Status: true" },
"steps": [
+2 -2
View File
@@ -4,12 +4,12 @@ services:
container_name: dbx-kafka-4.3
restart: always
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-9093}:9092"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-11300}:9092"
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: EXTERNAL://0.0.0.0:9092,CONTROLLER://0.0.0.0:9094,INTERNAL://0.0.0.0:9095
KAFKA_ADVERTISED_LISTENERS: EXTERNAL://${KAFKA_ADVERTISED_HOST:-localhost}:${DB_PORT:-9093},INTERNAL://database:9095
KAFKA_ADVERTISED_LISTENERS: EXTERNAL://${KAFKA_ADVERTISED_HOST:-localhost}:${DB_PORT:-11300},INTERNAL://database:9095
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,INTERNAL:PLAINTEXT
+2 -1
View File
@@ -7,7 +7,8 @@
"platforms": ["linux/amd64", "linux/arm64"],
"service": "database",
"defaultPort": 9092,
"connection": { "host": "127.0.0.1", "port": 9093, "authentication": "none", "database": "dbx", "internalPort": 9095 },
"connection": { "host": "127.0.0.1", "port": 11300, "authentication": "none", "database": "dbx", "internalPort": 9095 },
"hostPorts": { "DB_PORT": 11300 },
"shell": ["/bin/bash"],
"smoke": { "steps": [{ "name": "create, produce, and consume a smoke message", "command": ["/bin/bash", "-ec", "/opt/kafka/bin/kafka-topics.sh --bootstrap-server database:9095 --create --if-not-exists --topic dbx-smoke --partitions 1 --replication-factor 1; printf 'DBX smoke\\n' | /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server database:9095 --topic dbx-smoke; /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server database:9095 --topic dbx-smoke --from-beginning --max-messages 1 --timeout-ms 10000"], "expect": "DBX smoke" }] }
}
+1 -1
View File
@@ -4,7 +4,7 @@ services:
container_name: dbx-mariadb-10.11
restart: always
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-3307}:3306"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-10200}:3306"
environment:
MARIADB_ROOT_PASSWORD: "${DB_PASSWORD:-123456}"
MARIADB_DATABASE: dbx
+2 -1
View File
@@ -7,7 +7,8 @@
"platforms": ["linux/amd64", "linux/arm64"],
"service": "database",
"defaultPort": 3306,
"connection": { "host": "127.0.0.1", "port": 3307, "username": "root", "password": "123456", "database": "dbx" },
"connection": { "host": "127.0.0.1", "port": 10200, "username": "root", "password": "123456", "database": "dbx" },
"hostPorts": { "DB_PORT": 10200 },
"shell": ["mariadb", "-uroot", "-p${DB_PASSWORD}", "-Ddbx"],
"smoke": { "steps": [{ "name": "query initialized row", "command": ["mariadb", "-uroot", "-p${DB_PASSWORD}", "-Ddbx", "-Nse", "SELECT note FROM dbx_smoke WHERE id=1"], "expect": "DBX smoke" }] }
}
+1 -1
View File
@@ -4,7 +4,7 @@ services:
container_name: dbx-mongodb-5.0
restart: always
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-27018}:27017"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-10400}:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: "${DB_PASSWORD:-123456}"
+2 -1
View File
@@ -7,7 +7,8 @@
"platforms": ["linux/amd64", "linux/arm64"],
"service": "database",
"defaultPort": 27017,
"connection": { "host": "127.0.0.1", "port": 27018, "username": "root", "password": "123456", "database": "dbx", "authSource": "admin" },
"connection": { "host": "127.0.0.1", "port": 10400, "username": "root", "password": "123456", "database": "dbx", "authSource": "admin" },
"hostPorts": { "DB_PORT": 10400 },
"shell": ["mongosh", "--quiet", "--host", "127.0.0.1", "--port", "27017", "--username", "root", "--password", "${DB_PASSWORD}", "--authenticationDatabase", "admin", "dbx"],
"smoke": { "steps": [{ "name": "query initialized document over TCP", "command": ["mongosh", "--quiet", "--host", "127.0.0.1", "--port", "27017", "--username", "root", "--password", "${DB_PASSWORD}", "--authenticationDatabase", "admin", "dbx", "--eval", "db.dbx_smoke.findOne({_id: 1}).note"], "expect": "DBX smoke" }] }
}
+1 -1
View File
@@ -4,7 +4,7 @@ services:
container_name: dbx-mongodb-8.2
restart: always
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-27018}:27017"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-10401}:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: "${DB_PASSWORD:-123456}"
+2 -1
View File
@@ -7,7 +7,8 @@
"platforms": ["linux/amd64", "linux/arm64"],
"service": "database",
"defaultPort": 27017,
"connection": { "host": "127.0.0.1", "port": 27018, "username": "root", "password": "123456", "database": "dbx", "authSource": "admin" },
"connection": { "host": "127.0.0.1", "port": 10401, "username": "root", "password": "123456", "database": "dbx", "authSource": "admin" },
"hostPorts": { "DB_PORT": 10401 },
"shell": ["mongosh", "--quiet", "--host", "127.0.0.1", "--port", "27017", "--username", "root", "--password", "${DB_PASSWORD}", "--authenticationDatabase", "admin", "dbx"],
"smoke": { "steps": [{ "name": "query initialized document over TCP", "command": ["mongosh", "--quiet", "--host", "127.0.0.1", "--port", "27017", "--username", "root", "--password", "${DB_PASSWORD}", "--authenticationDatabase", "admin", "dbx", "--eval", "db.dbx_smoke.findOne({_id: 1}).note"], "expect": "DBX smoke" }] }
}
+1 -1
View File
@@ -4,7 +4,7 @@ services:
container_name: dbx-mysql-5.7
restart: always
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-3307}:3306"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-10100}:3306"
environment:
MYSQL_ROOT_PASSWORD: "${DB_PASSWORD:-123456}"
MYSQL_DATABASE: dbx
+2 -1
View File
@@ -7,7 +7,8 @@
"platforms": ["linux/amd64", "linux/arm64"],
"service": "database",
"defaultPort": 3306,
"connection": { "host": "127.0.0.1", "port": 3307, "username": "root", "password": "123456", "database": "dbx" },
"connection": { "host": "127.0.0.1", "port": 10100, "username": "root", "password": "123456", "database": "dbx" },
"hostPorts": { "DB_PORT": 10100 },
"shell": ["mysql", "-uroot", "-p${DB_PASSWORD}", "-Ddbx"],
"smoke": { "steps": [{ "name": "query initialized row", "command": ["mysql", "-uroot", "-p${DB_PASSWORD}", "-Ddbx", "-Nse", "SELECT note FROM dbx_smoke WHERE id=1"], "expect": "DBX smoke" }] }
}
+1 -1
View File
@@ -4,7 +4,7 @@ services:
container_name: dbx-mysql-8.4
restart: always
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-3307}:3306"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-10101}:3306"
environment:
MYSQL_ROOT_PASSWORD: "${DB_PASSWORD:-123456}"
MYSQL_DATABASE: dbx
+2 -1
View File
@@ -7,7 +7,8 @@
"platforms": ["linux/amd64", "linux/arm64"],
"service": "database",
"defaultPort": 3306,
"connection": { "host": "127.0.0.1", "port": 3307, "username": "root", "password": "123456", "database": "dbx" },
"connection": { "host": "127.0.0.1", "port": 10101, "username": "root", "password": "123456", "database": "dbx" },
"hostPorts": { "DB_PORT": 10101 },
"shell": ["mysql", "-uroot", "-p${DB_PASSWORD}", "-Ddbx"],
"smoke": { "steps": [{ "name": "query initialized row", "command": ["mysql", "-uroot", "-p${DB_PASSWORD}", "-Ddbx", "-Nse", "SELECT note FROM dbx_smoke WHERE id=1"], "expect": "DBX smoke" }] }
}
+3 -3
View File
@@ -4,9 +4,9 @@ services:
container_name: dbx-nacos-2.5
restart: always
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-8849}:8848"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${NACOS_GRPC_PORT:-9849}:9848"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${NACOS_RAFT_PORT:-9850}:9849"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-11000}:8848"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${NACOS_GRPC_PORT:-11001}:9848"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${NACOS_RAFT_PORT:-11002}:9849"
environment:
MODE: standalone
PREFER_HOST_MODE: hostname
+2 -1
View File
@@ -7,7 +7,8 @@
"platforms": ["linux/amd64", "linux/arm64"],
"service": "database",
"defaultPort": 8848,
"connection": { "host": "127.0.0.1", "port": 8849, "username": "nacos", "password": "123456", "database": "dbx", "namespace": "public" },
"connection": { "host": "127.0.0.1", "port": 11000, "username": "nacos", "password": "123456", "database": "dbx", "namespace": "public", "grpcPort": 11001, "raftPort": 11002 },
"hostPorts": { "DB_PORT": 11000, "NACOS_GRPC_PORT": 11001, "NACOS_RAFT_PORT": 11002 },
"bootstrap": {
"check": { "command": ["curl", "--fail", "--silent", "--show-error", "--request", "POST", "http://127.0.0.1:8848/nacos/v1/auth/login", "--data-urlencode", "username=nacos", "--data-urlencode", "password=${DB_PASSWORD}"], "expect": "accessToken" },
"steps": [{ "name": "initialize administrator password", "command": ["sh", "-ec", "curl --fail --silent --show-error --request POST --data-urlencode \"password=$1\" http://127.0.0.1:8848/nacos/v1/auth/users/admin >/dev/null; curl --fail --silent --show-error --request POST http://127.0.0.1:8848/nacos/v1/auth/login --data-urlencode username=nacos --data-urlencode \"password=$1\" | grep -q '\"accessToken\"'; printf 'administrator initialized\\n'", "sh", "${DB_PASSWORD}"], "expect": "administrator initialized" }]
+5 -5
View File
@@ -1,13 +1,13 @@
services:
database:
image: docker.cnb.cool/znb/images/nacos-server:v3.2.2
image: docker.cnb.cool/znb/images/nacos-server:v3.2.3
container_name: dbx-nacos-3.2
restart: always
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${NACOS_CONSOLE_PORT:-8010}:8080"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-8849}:8848"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${NACOS_GRPC_PORT:-9849}:9848"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${NACOS_RAFT_PORT:-9850}:9849"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${NACOS_CONSOLE_PORT:-11004}:8080"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-11003}:8848"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${NACOS_GRPC_PORT:-11005}:9848"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${NACOS_RAFT_PORT:-11006}:9849"
environment:
MODE: standalone
PREFER_HOST_MODE: hostname
+4 -3
View File
@@ -1,13 +1,14 @@
{
"database": "nacos",
"name": "Nacos",
"version": "3.2.2",
"version": "3.2.3",
"displayVersion": "3.2",
"image": "docker.cnb.cool/znb/images/nacos-server:v3.2.2",
"image": "docker.cnb.cool/znb/images/nacos-server:v3.2.3",
"platforms": ["linux/amd64", "linux/arm64"],
"service": "database",
"defaultPort": 8848,
"connection": { "host": "127.0.0.1", "port": 8849, "username": "nacos", "password": "123456", "database": "dbx", "namespace": "public", "consolePort": 8010 },
"connection": { "host": "127.0.0.1", "port": 11003, "username": "nacos", "password": "123456", "database": "dbx", "namespace": "public", "consolePort": 11004, "grpcPort": 11005, "raftPort": 11006 },
"hostPorts": { "DB_PORT": 11003, "NACOS_CONSOLE_PORT": 11004, "NACOS_GRPC_PORT": 11005, "NACOS_RAFT_PORT": 11006 },
"bootstrap": {
"check": { "command": ["curl", "--fail", "--silent", "--show-error", "--request", "POST", "http://127.0.0.1:8848/nacos/v3/auth/user/login", "--data-urlencode", "username=nacos", "--data-urlencode", "password=${DB_PASSWORD}"], "expect": "accessToken" },
"steps": [{ "name": "initialize administrator password", "command": ["sh", "-ec", "curl --fail --silent --show-error --request POST --data-urlencode \"password=$1\" http://127.0.0.1:8848/nacos/v3/auth/user/admin >/dev/null; curl --fail --silent --show-error --request POST http://127.0.0.1:8848/nacos/v3/auth/user/login --data-urlencode username=nacos --data-urlencode \"password=$1\" | grep -q '\"accessToken\"'; printf 'administrator initialized\\n'", "sh", "${DB_PASSWORD}"], "expect": "administrator initialized" }]
@@ -4,7 +4,7 @@ services:
container_name: dbx-postgresql-14.23
restart: always
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-5433}:5432"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-10300}:5432"
environment:
POSTGRES_PASSWORD: "${DB_PASSWORD:-123456}"
POSTGRES_DB: dbx
+2 -1
View File
@@ -7,7 +7,8 @@
"platforms": ["linux/amd64", "linux/arm64"],
"service": "database",
"defaultPort": 5432,
"connection": { "host": "127.0.0.1", "port": 5433, "username": "postgres", "password": "123456", "database": "dbx" },
"connection": { "host": "127.0.0.1", "port": 10300, "username": "postgres", "password": "123456", "database": "dbx" },
"hostPorts": { "DB_PORT": 10300 },
"shell": ["env", "PGPASSWORD=${DB_PASSWORD}", "psql", "-h", "127.0.0.1", "-p", "5432", "-U", "postgres", "-d", "dbx"],
"smoke": { "steps": [{ "name": "query initialized row over TCP", "command": ["env", "PGPASSWORD=${DB_PASSWORD}", "psql", "-h", "127.0.0.1", "-p", "5432", "-U", "postgres", "-d", "dbx", "-Atc", "SELECT note FROM dbx_smoke WHERE id=1"], "expect": "DBX smoke" }] }
}
+1 -1
View File
@@ -4,7 +4,7 @@ services:
container_name: dbx-postgresql-17.4
restart: always
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-5433}:5432"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-10301}:5432"
environment:
POSTGRES_PASSWORD: "${DB_PASSWORD:-123456}"
POSTGRES_DB: dbx
+2 -1
View File
@@ -7,7 +7,8 @@
"platforms": ["linux/amd64", "linux/arm64"],
"service": "database",
"defaultPort": 5432,
"connection": { "host": "127.0.0.1", "port": 5433, "username": "postgres", "password": "123456", "database": "dbx" },
"connection": { "host": "127.0.0.1", "port": 10301, "username": "postgres", "password": "123456", "database": "dbx" },
"hostPorts": { "DB_PORT": 10301 },
"shell": ["env", "PGPASSWORD=${DB_PASSWORD}", "psql", "-h", "127.0.0.1", "-p", "5432", "-U", "postgres", "-d", "dbx"],
"smoke": { "steps": [{ "name": "query initialized row over TCP", "command": ["env", "PGPASSWORD=${DB_PASSWORD}", "psql", "-h", "127.0.0.1", "-p", "5432", "-U", "postgres", "-d", "dbx", "-Atc", "SELECT note FROM dbx_smoke WHERE id=1"], "expect": "DBX smoke" }] }
}
+2 -2
View File
@@ -5,8 +5,8 @@ services:
restart: always
command: ["bin/pulsar", "standalone", "--advertised-address", "localhost"]
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-6651}:6650"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${PULSAR_WEB_PORT:-8081}:8080"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-11400}:6650"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${PULSAR_WEB_PORT:-11401}:8080"
volumes:
- data:/pulsar/data
- conf:/pulsar/conf
+2 -1
View File
@@ -7,7 +7,8 @@
"platforms": ["linux/amd64", "linux/arm64"],
"service": "database",
"defaultPort": 6650,
"connection": { "host": "127.0.0.1", "port": 6651, "authentication": "none", "database": "dbx", "namespace": "public/default", "webPort": 8081 },
"connection": { "host": "127.0.0.1", "port": 11400, "authentication": "none", "database": "dbx", "namespace": "public/default", "webPort": 11401 },
"hostPorts": { "DB_PORT": 11400, "PULSAR_WEB_PORT": 11401 },
"shell": ["/bin/bash"],
"smoke": { "steps": [{ "name": "produce a smoke message", "command": ["bin/pulsar-client", "produce", "persistent://public/default/dbx-smoke", "--messages", "DBX smoke"], "expect": "1 messages successfully produced" }] }
}
+2 -2
View File
@@ -4,8 +4,8 @@ services:
container_name: dbx-qdrant-1.8
restart: always
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-6334}:6333"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${QDRANT_GRPC_PORT:-6335}:6334"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-11200}:6333"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${QDRANT_GRPC_PORT:-11201}:6334"
environment:
QDRANT__SERVICE__API_KEY: "${DB_PASSWORD:-123456}"
volumes:
+2 -1
View File
@@ -7,7 +7,8 @@
"platforms": ["linux/amd64", "linux/arm64"],
"service": "database",
"defaultPort": 6333,
"connection": { "host": "127.0.0.1", "port": 6334, "username": "", "password": "123456", "database": "dbx", "grpcPort": 6335 },
"connection": { "host": "127.0.0.1", "port": 11200, "username": "", "password": "123456", "database": "dbx", "grpcPort": 11201 },
"hostPorts": { "DB_PORT": 11200, "QDRANT_GRPC_PORT": 11201 },
"shell": ["bash"],
"smoke": { "steps": [{ "name": "reject an unauthenticated collection request", "command": ["bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/6333; printf 'GET /collections HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3; cat <&3"], "expect": "403 Forbidden" }, { "name": "query collections with the configured API key", "command": ["bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/6333; printf 'GET /collections HTTP/1.1\\r\\nHost: localhost\\r\\napi-key: %s\\r\\nConnection: close\\r\\n\\r\\n' \"$1\" >&3; cat <&3", "bash", "${DB_PASSWORD}"], "expect": "collections" }] }
}
+1 -1
View File
@@ -9,7 +9,7 @@ services:
# Keep the password in container env so CMD-SHELL can quote it safely.
REDIS_PASSWORD: "${DB_PASSWORD:-123456}"
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-6380}:6379"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-10500}:6379"
volumes:
- data:/data
healthcheck:
+2 -1
View File
@@ -7,7 +7,8 @@
"platforms": ["linux/amd64"],
"service": "database",
"defaultPort": 6379,
"connection": { "host": "127.0.0.1", "port": 6380, "password": "123456", "database": 0 },
"connection": { "host": "127.0.0.1", "port": 10500, "password": "123456", "database": 0 },
"hostPorts": { "DB_PORT": 10500 },
"shell": ["redis-cli", "-a", "${DB_PASSWORD}"],
"smoke": { "steps": [{ "name": "seed smoke key", "command": ["redis-cli", "-a", "${DB_PASSWORD}", "SET", "dbx:smoke", "DBX smoke"], "expect": "OK" }, { "name": "read smoke key", "command": ["redis-cli", "-a", "${DB_PASSWORD}", "GET", "dbx:smoke"], "expect": "DBX smoke" }] }
}
+1 -1
View File
@@ -8,7 +8,7 @@ services:
# Keep the password in container env so CMD-SHELL can quote it safely.
REDIS_PASSWORD: "${DB_PASSWORD:-123456}"
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-6380}:6379"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-10501}:6379"
volumes:
- data:/data
healthcheck:
+2 -1
View File
@@ -7,7 +7,8 @@
"platforms": ["linux/amd64", "linux/arm64"],
"service": "database",
"defaultPort": 6379,
"connection": { "host": "127.0.0.1", "port": 6380, "username": "default", "password": "123456", "database": 0 },
"connection": { "host": "127.0.0.1", "port": 10501, "username": "default", "password": "123456", "database": 0 },
"hostPorts": { "DB_PORT": 10501 },
"shell": ["redis-cli", "-a", "${DB_PASSWORD}"],
"smoke": { "steps": [{ "name": "seed smoke key", "command": ["redis-cli", "-a", "${DB_PASSWORD}", "SET", "dbx:smoke", "DBX smoke"], "expect": "OK" }, { "name": "read smoke key", "command": ["redis-cli", "-a", "${DB_PASSWORD}", "GET", "dbx:smoke"], "expect": "DBX smoke" }] }
}
+3 -3
View File
@@ -3,9 +3,9 @@ services:
image: docker.cnb.cool/znb/images/rnacos:v0.8.5
container_name: dbx-rnacos-0.8
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-8849}:8848"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${RNACOS_GRPC_PORT:-9849}:9848"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${RNACOS_CONSOLE_PORT:-10849}:10848"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-11100}:8848"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${RNACOS_GRPC_PORT:-11101}:9848"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${RNACOS_CONSOLE_PORT:-11102}:10848"
volumes:
- data:/io
environment:
+2 -1
View File
@@ -7,7 +7,8 @@
"platforms": ["linux/amd64", "linux/arm64"],
"service": "database",
"defaultPort": 8848,
"connection": { "host": "127.0.0.1", "port": 8849, "username": "admin", "password": "123456", "database": "dbx", "namespace": "public", "grpcPort": 9849, "consolePort": 10849 },
"connection": { "host": "127.0.0.1", "port": 11100, "username": "admin", "password": "123456", "database": "dbx", "namespace": "public", "grpcPort": 11101, "consolePort": 11102 },
"hostPorts": { "DB_PORT": 11100, "RNACOS_GRPC_PORT": 11101, "RNACOS_CONSOLE_PORT": 11102 },
"shell": ["bash"],
"smoke": { "steps": [{ "name": "check the HTTP health endpoint", "command": ["bash", "-ec", "exec 3<>/dev/tcp/127.0.0.1/8848; printf 'GET /health HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3; cat <&3"], "expect": "success" }] }
}
+1 -1
View File
@@ -4,7 +4,7 @@ services:
container_name: dbx-zookeeper-3.9
restart: always
ports:
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-2182}:2181"
- "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-10800}:2181"
volumes:
- data:/data
- datalog:/datalog
+2 -1
View File
@@ -7,7 +7,8 @@
"platforms": ["linux/amd64", "linux/arm64"],
"service": "database",
"defaultPort": 2181,
"connection": { "host": "127.0.0.1", "port": 2182, "username": "root", "password": "123456", "database": "dbx", "authScheme": "digest" },
"connection": { "host": "127.0.0.1", "port": 10800, "username": "root", "password": "123456", "database": "dbx", "authScheme": "digest" },
"hostPorts": { "DB_PORT": 10800 },
"bootstrap": {
"check": { "command": ["sh", "-ec", "printf 'addauth digest root:%s\\nget /dbx\\nquit\\n' \"$1\" | zkCli.sh -server 127.0.0.1:2181", "sh", "${DB_PASSWORD}"], "expect": "DBX smoke" },
"steps": [{ "name": "create Digest-protected smoke node", "command": ["sh", "-ec", "printf 'addauth digest root:%s\\ncreate /dbx \"DBX smoke\" auth:root:%s:cdrwa\\nquit\\n' \"$1\" \"$1\" | zkCli.sh -server 127.0.0.1:2181", "sh", "${DB_PASSWORD}"], "expect": "Created /dbx" }]
+47 -31
View File
@@ -9,63 +9,77 @@ description: 为 DBX 开发与验证启动可重复创建的本地数据库实
## 可用配方
| 数据库 | 版本目录 | Docker Hub 官方镜像 | Compose 使用的 CNB 镜像 | 默认宿主端口 | 容器名 |
| 数据库 | 版本目录 | 官方上游镜像 | Compose 使用的 CNB 镜像 | 默认宿主端口 | 容器名 |
| --- | --- | --- | --- | ---: | --- |
| ClickHouse | `clickhouse/24.8` | `clickhouse/clickhouse-server:24.8.14.39` | `docker.cnb.cool/znb/images/clickhouse-server:24.8.14.39` | `8124` | `dbx-clickhouse-24.8` |
| etcd | `etcd/3.7` | `gcr.io/etcd-development/etcd:v3.7.0` | `docker.cnb.cool/znb/images/etcd:v3.7.0` | `2380` | `dbx-etcd-3.7` |
| Kafka | `kafka/4.3` | `apache/kafka:4.3.1` | `docker.cnb.cool/znb/images/kafka:4.3.1` | `9093` | `dbx-kafka-4.3` |
| MariaDB | `mariadb/10.11` | `mariadb:10.11.11` | `docker.cnb.cool/znb/images/mariadb:10.11.11` | `3307` | `dbx-mariadb-10.11` |
| MongoDB | `mongodb/5.0` | `mongo:5.0.5` | `docker.cnb.cool/znb/images/mongo:5.0.5` | `27018` | `dbx-mongodb-5.0` |
| MongoDB | `mongodb/8.2` | `mongo:8.2.3-noble` | `docker.cnb.cool/znb/images/mongo:8.2.3-noble` | `27018` | `dbx-mongodb-8.2` |
| MySQL | `mysql/5.7` | `mysql:5.7.44` | `docker.cnb.cool/znb/images/mysql:5.7.44` | `3307` | `dbx-mysql-5.7` |
| MySQL | `mysql/8.4` | `mysql:8.4.6` | `docker.cnb.cool/znb/images/mysql:8.4.6` | `3307` | `dbx-mysql-8.4` |
| Nacos | `nacos/2.5` | `nacos/nacos-server:v2.5.2` | `docker.cnb.cool/znb/images/nacos-server:v2.5.2` | `8849` | `dbx-nacos-2.5` |
| Nacos | `nacos/3.2` | `nacos/nacos-server:v3.2.2` | `docker.cnb.cool/znb/images/nacos-server:v3.2.2` | `8849` | `dbx-nacos-3.2` |
| PostgreSQL | `postgresql/14.23` | `postgres:14.23` | `docker.cnb.cool/znb/images/postgres:14.23` | `5433` | `dbx-postgresql-14.23` |
| PostgreSQL | `postgresql/17.4` | `postgres:17.4` | `docker.cnb.cool/znb/images/postgres:17.4` | `5433` | `dbx-postgresql-17.4` |
| Pulsar | `pulsar/4.2` | `apachepulsar/pulsar:4.2.3` | `docker.cnb.cool/znb/images/pulsar:4.2.3` | `6651` | `dbx-pulsar-4.2` |
| Qdrant | `qdrant/1.8` | `qdrant/qdrant:v1.8.3` | `docker.cnb.cool/znb/images/qdrant:v1.8.3` | `6334` | `dbx-qdrant-1.8` |
| Redis | `redis/3.0.7` | `redis:3.0.7-alpine` | `docker.cnb.cool/znb/images/redis:3.0.7-alpine` | `6380` | `dbx-redis-3.0.7` |
| Redis | `redis/7.4` | `redis:7.4.9-alpine` | `docker.cnb.cool/znb/images/redis:7.4.9-alpine` | `6380` | `dbx-redis-7.4` |
| r-nacos | `rnacos/0.8` | `qingpan/rnacos:v0.8.5` | `docker.cnb.cool/znb/images/rnacos:v0.8.5` | `8849` | `dbx-rnacos-0.8` |
| ZooKeeper | `zookeeper/3.9` | `zookeeper:3.9.5` | `docker.cnb.cool/znb/images/zookeeper:3.9.5` | `2182` | `dbx-zookeeper-3.9` |
| ClickHouse | `clickhouse/24.8` | `clickhouse/clickhouse-server:24.8.14.39` | `docker.cnb.cool/znb/images/clickhouse-server:24.8.14.39` | `10600` | `dbx-clickhouse-24.8` |
| Consul | `consul/2.0.2` | `hashicorp/consul:2.0.2` | `docker.cnb.cool/znb/images/consul:2.0.2` | `10900` | `dbx-consul-2.0.2` |
| Elasticsearch | `elasticsearch/6.8` | `docker.elastic.co/elasticsearch/elasticsearch:6.8` | `docker.cnb.cool/znb/images/elasticsearch:6.8` | `11500` | `dbx-elasticsearch-6.8` |
| etcd | `etcd/3.7` | `gcr.io/etcd-development/etcd:v3.7.0` | `docker.cnb.cool/znb/images/etcd:v3.7.0` | `10700` | `dbx-etcd-3.7` |
| Kafka | `kafka/4.3` | `apache/kafka:4.3.1` | `docker.cnb.cool/znb/images/kafka:4.3.1` | `11300` | `dbx-kafka-4.3` |
| MariaDB | `mariadb/10.11` | `mariadb:10.11.11` | `docker.cnb.cool/znb/images/mariadb:10.11.11` | `10200` | `dbx-mariadb-10.11` |
| MongoDB | `mongodb/5.0` | `mongo:5.0.5` | `docker.cnb.cool/znb/images/mongo:5.0.5` | `10400` | `dbx-mongodb-5.0` |
| MongoDB | `mongodb/8.2` | `mongo:8.2.3-noble` | `docker.cnb.cool/znb/images/mongo:8.2.3-noble` | `10401` | `dbx-mongodb-8.2` |
| MySQL | `mysql/5.7` | `mysql:5.7.44` | `docker.cnb.cool/znb/images/mysql:5.7.44` | `10100` | `dbx-mysql-5.7` |
| MySQL | `mysql/8.4` | `mysql:8.4.6` | `docker.cnb.cool/znb/images/mysql:8.4.6` | `10101` | `dbx-mysql-8.4` |
| Nacos | `nacos/2.5` | `nacos/nacos-server:v2.5.2` | `docker.cnb.cool/znb/images/nacos-server:v2.5.2` | `11000` | `dbx-nacos-2.5` |
| Nacos | `nacos/3.2` | `nacos/nacos-server:v3.2.3` | `docker.cnb.cool/znb/images/nacos-server:v3.2.3` | `11003` | `dbx-nacos-3.2` |
| PostgreSQL | `postgresql/14.23` | `postgres:14.23` | `docker.cnb.cool/znb/images/postgres:14.23` | `10300` | `dbx-postgresql-14.23` |
| PostgreSQL | `postgresql/17.4` | `postgres:17.4` | `docker.cnb.cool/znb/images/postgres:17.4` | `10301` | `dbx-postgresql-17.4` |
| Pulsar | `pulsar/4.2` | `apachepulsar/pulsar:4.2.3` | `docker.cnb.cool/znb/images/pulsar:4.2.3` | `11400` | `dbx-pulsar-4.2` |
| Qdrant | `qdrant/1.8` | `qdrant/qdrant:v1.8.3` | `docker.cnb.cool/znb/images/qdrant:v1.8.3` | `11200` | `dbx-qdrant-1.8` |
| Redis | `redis/3.0.7` | `redis:3.0.7-alpine` | `docker.cnb.cool/znb/images/redis:3.0.7-alpine` | `10500` | `dbx-redis-3.0.7` |
| Redis | `redis/7.4` | `redis:7.4.9-alpine` | `docker.cnb.cool/znb/images/redis:7.4.9-alpine` | `10501` | `dbx-redis-7.4` |
| r-nacos | `rnacos/0.8` | `qingpan/rnacos:v0.8.5` | `docker.cnb.cool/znb/images/rnacos:v0.8.5` | `11100` | `dbx-rnacos-0.8` |
| ZooKeeper | `zookeeper/3.9` | `zookeeper:3.9.5` | `docker.cnb.cool/znb/images/zookeeper:3.9.5` | `10800` | `dbx-zookeeper-3.9` |
所有网络数据库的默认宿主端口都比标准端口大 `1`,并且该约定由配方校验器强制检查。默认密码统一为 `123456`,默认数据库名为 `dbx`。Redis 不支持命名数据库,因此使用 DB 0,并以 `dbx:` 作为冒烟键前缀。Compose 默认使用 CNB 镜像列;Docker Hub 列保留官方上游地址,便于核对来源或直接拉取。Redis 3.0.7 仅支持 amd64;在 arm64 主机上启动时会单独提醒,Docker 将使用模拟运行。其余配方均支持 amd64 和 arm64。
每个产品使用专属的 `101xx` 至 `115xx` 宿主端口段,校验器会验证全部声明的默认映射位于正确范围且全局唯一。默认密码统一为 `123456`,默认数据库名为 `dbx`。Redis 不支持命名数据库,因此使用 DB 0,并以 `dbx:` 作为冒烟键前缀。Compose 默认使用 CNB 镜像列;官方上游镜像列保留原始镜像地址,便于核对来源或直接拉取。Redis 3.0.7 仅支持 amd64;在 arm64 主机上启动时会单独提醒,Docker 将使用模拟运行。其余配方均支持 amd64 和 arm64。
Nacos 使用管理员用户名 `nacos`、密码 `123456` 和默认 `public` 命名空间。V3 配方额外在 `http://127.0.0.1:8010` 暴露 Web 控制台;V2 控制台使用主端口 `8849`。如需并行运行两个 Nacos 版本,请为其中一个版本设置 `NACOS_CONSOLE_PORT`、`NACOS_GRPC_PORT` 和 `NACOS_RAFT_PORT`。
Nacos 使用管理员用户名 `nacos`、密码 `123456` 和默认 `public` 命名空间。V2 的 HTTP、gRPC 和 Raft 宿主端口为 `11000`、`11001`、`11002`;V3 的 HTTP、Web 控制台、gRPC 和 Raft 宿主端口为 `11003`、`11004`、`11005`、`11006`。两个版本默认即可并行运行。
```bash
DB_PORT=8818 NACOS_CONSOLE_PORT=8010 NACOS_GRPC_PORT=9818 NACOS_RAFT_PORT=9819 make db DB=nacos@3.2
DB_PORT=11030 NACOS_CONSOLE_PORT=11031 NACOS_GRPC_PORT=11032 NACOS_RAFT_PORT=11033 make db DB=nacos@3.2
```
r-nacos 使用管理员账号 `admin` 和密码 `123456`。DBX 配方的 HTTP、gRPC 和 Web 控制台宿主端口默认分别为 `8849`、`9849`、`10849`;上游单机 Compose 使用 `8848:8848`、`9848:9848`、`10848:10848`。若需避开本机端口冲突,可使用以下自定义宿主端口映射(不是上游默认值):
r-nacos 使用管理员账号 `admin` 和密码 `123456`。DBX 配方的 HTTP、gRPC 和 Web 控制台宿主端口默认分别为 `11100`、`11101`、`11102`;上游单机 Compose 使用 `8848:8848`、`9848:9848`、`10848:10848`。
```bash
DB_PORT=3848 RNACOS_GRPC_PORT=3748 RNACOS_CONSOLE_PORT=3048 make db DB=rnacos@0.8
DB_PORT=11130 RNACOS_GRPC_PORT=11131 RNACOS_CONSOLE_PORT=11132 make db DB=rnacos@0.8
```
etcd 会创建密码为 `123456` 的 `root` 用户、授予 root 角色并开启认证。默认客户端和 peer 主机端口为 `2380`、`2381`。如需使用上游 Docker 命令中的端口,可执行:
etcd 会创建密码为 `123456` 的 `root` 用户、授予 root 角色并开启认证。默认客户端和 peer 主机端口为 `10700`、`10701`。如需使用上游 Docker 命令中的端口,可执行:
```bash
DB_PORT=2379 ETCD_PEER_PORT=2380 make db DB=etcd@3.7
```
Qdrant 将 `123456` 作为管理员 API Key。在 DBX 中请将用户名留空,并把 API Key 填入密码字段,驱动会通过 HTTP 请求头 `api-key` 发送。HTTP 和 gRPC 默认主机端口分别为 `6334`、`6335`。如需使用上游 Docker 命令中的端口,可执行:
Qdrant 将 `123456` 作为管理员 API Key。在 DBX 中请将用户名留空,并把 API Key 填入密码字段,驱动会通过 HTTP 请求头 `api-key` 发送。HTTP 和 gRPC 默认主机端口分别为 `11200`、`11201`。如需使用上游 Docker 命令中的端口,可执行:
```bash
DB_PORT=6333 QDRANT_GRPC_PORT=6334 make db DB=qdrant@1.8
```
ZooKeeper 使用 Digest 凭据 `root` / `123456` 保护 `/dbx` 节点。默认主机端口为 `2182`;设置 `DB_PORT=2181` 即可匹配上游 Docker 命令。ZooKeeper 的 Digest ACL 模型保护单个节点,并不提供全局登录开关。
ZooKeeper 使用 Digest 凭据 `root` / `123456` 保护 `/dbx` 节点。默认主机端口为 `10800`;设置 `DB_PORT=2181` 即可匹配上游 Docker 命令。ZooKeeper 的 Digest ACL 模型保护单个节点,并不提供全局登录开关。
Kafka 和 Pulsar 均为刻意保持未认证的单节点开发配方:Kafka 使用 PLAINTEXT,Pulsar 使用 standalone,因此不要将它们暴露到远程网络。Kafka 如需使用常用端口,可执行 `DB_PORT=9092 make db DB=kafka@4.3`;Pulsar 使用常用端口可执行 `DB_PORT=6650 PULSAR_WEB_PORT=8080 make db DB=pulsar@4.2`。
Redis 3.0.7 和 Redis 7.4.9 用于覆盖兼容性的两个端点。Redis 3 早于 ACL、RESP3、Streams 及许多现代命令,Redis 7 则覆盖当前协议与命令集。对这个实验室而言,同时保留 6.2 和 7.4 所提供的兼容性覆盖更小。
同一数据库的不同版本默认会共用宿主机端口。通常一次启动一个版本;需要并行对比时,为其中一个版本指定其他端口:
Consul 为单节点开发模式,使用 `agent -dev -client=0.0.0.0 -ui` 启动,不启用 ACL。HTTP API 与 UI、gRPC 和 DNS 的默认宿主端口分别为 `10900`、`10901` 和 `10902`;DNS 同时开放 TCP 和 UDP。以下命令可使用本地常用端口 `8500`、`8502` 和 `8600`:
```bash
DB_PORT=13307 make db DB=mysql@5.7
DB_PORT=8500 CONSUL_GRPC_PORT=8502 CONSUL_DNS_PORT=8600 make db DB=consul@2.0.2
```
Elasticsearch 6.8 以未认证的单节点开发集群运行,并禁用机器学习功能。HTTP API 和 transport 默认宿主端口为 `11500`、`11501`;以下命令可使用上游端口 `9200`、`9300`:
```bash
DB_PORT=9200 ELASTICSEARCH_TRANSPORT_PORT=9300 make db DB=elasticsearch@6.8
```
所有已提交配方都有唯一的默认宿主端口分配,因此版本和产品可默认并行运行。仅在本地规范需要不同端口时使用 `DB_PORT`:
```bash
DB_PORT=10150 make db DB=mysql@5.7
make db DB=mysql@8.4
```
@@ -79,15 +93,17 @@ make db-list
make db DB=postgresql@17.4
make db-verify DB=postgresql@17.4
make db DB=rnacos@0.8
make db DB=consul@2.0.2
make db DB=elasticsearch@6.8
```
不带参数执行 `make db` 会输出当前全部支持的、可直接复制启动的命令,以及可选参数。传入 `DB=product@version` 后,`db` 会等待 Compose 健康检查通过并输出 DBX 连接字段。`db-verify` 还会在数据库容器中执行配方中的冒烟命令,并校验预期输出;提交数据库相关修复前推荐执行此命令。
不带参数执行 `make db` 会输出当前全部支持的、可直接复制启动的命令,以及可选参数。传入 `DB=product@version` 后,`db` 会等待 Compose 健康检查通过并输出 DBX 连接字段;对于 DBX 已支持的连接类型,还会输出预填完成的 `dbx://connection/new` 深链。复制该链接到浏览器或终端执行 `open '<链接>'`,即可打开桌面端的新建连接窗口。使用前请先安装并启动一次 DBX 桌面端以注册 `dbx://` 协议。深链可能包含密码,因此不要将其写入共享终端历史、日志或工单。尚未具备 DBX 连接类型的配方会明确显示深链不可用,例如 Consul、Nacos、Kafka、Pulsar 和 r-nacos。`db-verify` 还会在数据库容器中执行配方中的冒烟命令,并校验预期输出;提交数据库相关修复前推荐执行此命令。
## 命令和变量
| Make 目标 | 用途 |
| --- | --- |
| `make db-list` | 列出数据库版本、镜像和支持的平台 |
| `make db-list` | 列出数据库版本、容器到宿主机端口映射、镜像和支持的平台 |
| `make db DB=product@version` | 创建、等待环境就绪并输出连接字段 |
| `make db-verify DB=product@version` | 启动并运行冒烟检查 |
| `make db-down DB=product@version` | 停止环境但保留数据卷 |
+47 -31
View File
@@ -9,63 +9,77 @@ The Database Test Lab provides small, pinned Docker Compose environments for tes
## Available recipes
| Database | Version directory | Official Docker Hub image | CNB image used by Compose | Default host port | Container name |
| Database | Version directory | Official upstream image | CNB image used by Compose | Default host port | Container name |
| --- | --- | --- | --- | ---: | --- |
| ClickHouse | `clickhouse/24.8` | `clickhouse/clickhouse-server:24.8.14.39` | `docker.cnb.cool/znb/images/clickhouse-server:24.8.14.39` | `8124` | `dbx-clickhouse-24.8` |
| etcd | `etcd/3.7` | `gcr.io/etcd-development/etcd:v3.7.0` | `docker.cnb.cool/znb/images/etcd:v3.7.0` | `2380` | `dbx-etcd-3.7` |
| Kafka | `kafka/4.3` | `apache/kafka:4.3.1` | `docker.cnb.cool/znb/images/kafka:4.3.1` | `9093` | `dbx-kafka-4.3` |
| MariaDB | `mariadb/10.11` | `mariadb:10.11.11` | `docker.cnb.cool/znb/images/mariadb:10.11.11` | `3307` | `dbx-mariadb-10.11` |
| MongoDB | `mongodb/5.0` | `mongo:5.0.5` | `docker.cnb.cool/znb/images/mongo:5.0.5` | `27018` | `dbx-mongodb-5.0` |
| MongoDB | `mongodb/8.2` | `mongo:8.2.3-noble` | `docker.cnb.cool/znb/images/mongo:8.2.3-noble` | `27018` | `dbx-mongodb-8.2` |
| MySQL | `mysql/5.7` | `mysql:5.7.44` | `docker.cnb.cool/znb/images/mysql:5.7.44` | `3307` | `dbx-mysql-5.7` |
| MySQL | `mysql/8.4` | `mysql:8.4.6` | `docker.cnb.cool/znb/images/mysql:8.4.6` | `3307` | `dbx-mysql-8.4` |
| Nacos | `nacos/2.5` | `nacos/nacos-server:v2.5.2` | `docker.cnb.cool/znb/images/nacos-server:v2.5.2` | `8849` | `dbx-nacos-2.5` |
| Nacos | `nacos/3.2` | `nacos/nacos-server:v3.2.2` | `docker.cnb.cool/znb/images/nacos-server:v3.2.2` | `8849` | `dbx-nacos-3.2` |
| PostgreSQL | `postgresql/14.23` | `postgres:14.23` | `docker.cnb.cool/znb/images/postgres:14.23` | `5433` | `dbx-postgresql-14.23` |
| PostgreSQL | `postgresql/17.4` | `postgres:17.4` | `docker.cnb.cool/znb/images/postgres:17.4` | `5433` | `dbx-postgresql-17.4` |
| Pulsar | `pulsar/4.2` | `apachepulsar/pulsar:4.2.3` | `docker.cnb.cool/znb/images/pulsar:4.2.3` | `6651` | `dbx-pulsar-4.2` |
| Qdrant | `qdrant/1.8` | `qdrant/qdrant:v1.8.3` | `docker.cnb.cool/znb/images/qdrant:v1.8.3` | `6334` | `dbx-qdrant-1.8` |
| Redis | `redis/3.0.7` | `redis:3.0.7-alpine` | `docker.cnb.cool/znb/images/redis:3.0.7-alpine` | `6380` | `dbx-redis-3.0.7` |
| Redis | `redis/7.4` | `redis:7.4.9-alpine` | `docker.cnb.cool/znb/images/redis:7.4.9-alpine` | `6380` | `dbx-redis-7.4` |
| r-nacos | `rnacos/0.8` | `qingpan/rnacos:v0.8.5` | `docker.cnb.cool/znb/images/rnacos:v0.8.5` | `8849` | `dbx-rnacos-0.8` |
| ZooKeeper | `zookeeper/3.9` | `zookeeper:3.9.5` | `docker.cnb.cool/znb/images/zookeeper:3.9.5` | `2182` | `dbx-zookeeper-3.9` |
| ClickHouse | `clickhouse/24.8` | `clickhouse/clickhouse-server:24.8.14.39` | `docker.cnb.cool/znb/images/clickhouse-server:24.8.14.39` | `10600` | `dbx-clickhouse-24.8` |
| Consul | `consul/2.0.2` | `hashicorp/consul:2.0.2` | `docker.cnb.cool/znb/images/consul:2.0.2` | `10900` | `dbx-consul-2.0.2` |
| Elasticsearch | `elasticsearch/6.8` | `docker.elastic.co/elasticsearch/elasticsearch:6.8` | `docker.cnb.cool/znb/images/elasticsearch:6.8` | `11500` | `dbx-elasticsearch-6.8` |
| etcd | `etcd/3.7` | `gcr.io/etcd-development/etcd:v3.7.0` | `docker.cnb.cool/znb/images/etcd:v3.7.0` | `10700` | `dbx-etcd-3.7` |
| Kafka | `kafka/4.3` | `apache/kafka:4.3.1` | `docker.cnb.cool/znb/images/kafka:4.3.1` | `11300` | `dbx-kafka-4.3` |
| MariaDB | `mariadb/10.11` | `mariadb:10.11.11` | `docker.cnb.cool/znb/images/mariadb:10.11.11` | `10200` | `dbx-mariadb-10.11` |
| MongoDB | `mongodb/5.0` | `mongo:5.0.5` | `docker.cnb.cool/znb/images/mongo:5.0.5` | `10400` | `dbx-mongodb-5.0` |
| MongoDB | `mongodb/8.2` | `mongo:8.2.3-noble` | `docker.cnb.cool/znb/images/mongo:8.2.3-noble` | `10401` | `dbx-mongodb-8.2` |
| MySQL | `mysql/5.7` | `mysql:5.7.44` | `docker.cnb.cool/znb/images/mysql:5.7.44` | `10100` | `dbx-mysql-5.7` |
| MySQL | `mysql/8.4` | `mysql:8.4.6` | `docker.cnb.cool/znb/images/mysql:8.4.6` | `10101` | `dbx-mysql-8.4` |
| Nacos | `nacos/2.5` | `nacos/nacos-server:v2.5.2` | `docker.cnb.cool/znb/images/nacos-server:v2.5.2` | `11000` | `dbx-nacos-2.5` |
| Nacos | `nacos/3.2` | `nacos/nacos-server:v3.2.3` | `docker.cnb.cool/znb/images/nacos-server:v3.2.3` | `11003` | `dbx-nacos-3.2` |
| PostgreSQL | `postgresql/14.23` | `postgres:14.23` | `docker.cnb.cool/znb/images/postgres:14.23` | `10300` | `dbx-postgresql-14.23` |
| PostgreSQL | `postgresql/17.4` | `postgres:17.4` | `docker.cnb.cool/znb/images/postgres:17.4` | `10301` | `dbx-postgresql-17.4` |
| Pulsar | `pulsar/4.2` | `apachepulsar/pulsar:4.2.3` | `docker.cnb.cool/znb/images/pulsar:4.2.3` | `11400` | `dbx-pulsar-4.2` |
| Qdrant | `qdrant/1.8` | `qdrant/qdrant:v1.8.3` | `docker.cnb.cool/znb/images/qdrant:v1.8.3` | `11200` | `dbx-qdrant-1.8` |
| Redis | `redis/3.0.7` | `redis:3.0.7-alpine` | `docker.cnb.cool/znb/images/redis:3.0.7-alpine` | `10500` | `dbx-redis-3.0.7` |
| Redis | `redis/7.4` | `redis:7.4.9-alpine` | `docker.cnb.cool/znb/images/redis:7.4.9-alpine` | `10501` | `dbx-redis-7.4` |
| r-nacos | `rnacos/0.8` | `qingpan/rnacos:v0.8.5` | `docker.cnb.cool/znb/images/rnacos:v0.8.5` | `11100` | `dbx-rnacos-0.8` |
| ZooKeeper | `zookeeper/3.9` | `zookeeper:3.9.5` | `docker.cnb.cool/znb/images/zookeeper:3.9.5` | `10800` | `dbx-zookeeper-3.9` |
Every network recipe uses a host port one greater than the database standard port, and the recipe validator enforces that contract. The default password is `123456`, and the default database is `dbx`. Redis has no named databases, so it uses DB 0 with a `dbx:` smoke-key prefix. Compose uses the CNB image column; the Docker Hub column preserves the official upstream reference for provenance and direct pulls. Redis 3.0.7 is amd64-only; on arm64 hosts the command prints an architecture warning and Docker uses emulation. Every other recipe supports both amd64 and arm64.
Each product uses a dedicated `101xx`-`115xx` host-port range, and the validator verifies that every declared default mapping is in the correct range and globally unique. The default password is `123456`, and the default database is `dbx`. Redis has no named databases, so it uses DB 0 with a `dbx:` smoke-key prefix. Compose uses the CNB image column; the official upstream image column preserves the original image reference for provenance and direct pulls. Redis 3.0.7 is amd64-only; on arm64 hosts the command prints an architecture warning and Docker uses emulation. Every other recipe supports both amd64 and arm64.
Nacos uses the `nacos` administrator, password `123456`, and its default `public` namespace. Its V3 recipe also exposes the web console at `http://127.0.0.1:8010`; V2 serves its console on the main `8849` port. Set `NACOS_CONSOLE_PORT`, `NACOS_GRPC_PORT`, and `NACOS_RAFT_PORT` when running an Nacos version alongside another one.
Nacos uses the `nacos` administrator, password `123456`, and its default `public` namespace. V2 uses HTTP, gRPC, and Raft host ports `11000`, `11001`, and `11002`; V3 uses HTTP `11003`, web console `11004`, gRPC `11005`, and Raft `11006`. The two versions can run together by default.
```bash
DB_PORT=8818 NACOS_CONSOLE_PORT=8010 NACOS_GRPC_PORT=9818 NACOS_RAFT_PORT=9819 make db DB=nacos@3.2
DB_PORT=11030 NACOS_CONSOLE_PORT=11031 NACOS_GRPC_PORT=11032 NACOS_RAFT_PORT=11033 make db DB=nacos@3.2
```
r-nacos uses the `admin` administrator and password `123456`. The DBX recipe defaults its HTTP, gRPC, and web-console host ports to `8849`, `9849`, and `10849`; the upstream standalone Compose file publishes `8848:8848`, `9848:9848`, and `10848:10848`. To avoid local port conflicts, use this custom host-port mapping (not the upstream defaults):
r-nacos uses the `admin` administrator and password `123456`. The DBX recipe defaults its HTTP, gRPC, and web-console host ports to `11100`, `11101`, and `11102`; the upstream standalone Compose file publishes `8848:8848`, `9848:9848`, and `10848:10848`.
```bash
DB_PORT=3848 RNACOS_GRPC_PORT=3748 RNACOS_CONSOLE_PORT=3048 make db DB=rnacos@0.8
DB_PORT=11130 RNACOS_GRPC_PORT=11131 RNACOS_CONSOLE_PORT=11132 make db DB=rnacos@0.8
```
etcd creates the `root` user with password `123456`, grants the root role, then enables authentication. Its default client and peer ports are `2380` and `2381`. To use the upstream Docker command's ports, run:
etcd creates the `root` user with password `123456`, grants the root role, then enables authentication. Its default client and peer ports are `10700` and `10701`. To use the upstream Docker command's ports, run:
```bash
DB_PORT=2379 ETCD_PEER_PORT=2380 make db DB=etcd@3.7
```
Qdrant uses `123456` as its administrator API key. In DBX, leave the username empty and enter the key as the password so the driver sends it in the `api-key` HTTP header. Its HTTP and gRPC ports default to `6334` and `6335`. To use the ports in the upstream Docker command, run:
Qdrant uses `123456` as its administrator API key. In DBX, leave the username empty and enter the key as the password so the driver sends it in the `api-key` HTTP header. Its HTTP and gRPC ports default to `11200` and `11201`. To use the ports in the upstream Docker command, run:
```bash
DB_PORT=6333 QDRANT_GRPC_PORT=6334 make db DB=qdrant@1.8
```
ZooKeeper uses Digest credentials `root` / `123456` to protect the `/dbx` node. Its default host port is `2182`; use `DB_PORT=2181` to match the upstream Docker command. ZooKeeper's Digest ACL model protects individual nodes rather than providing a global login switch.
ZooKeeper uses Digest credentials `root` / `123456` to protect the `/dbx` node. Its default host port is `10800`; use `DB_PORT=2181` to match the upstream Docker command. ZooKeeper's Digest ACL model protects individual nodes rather than providing a global login switch.
Kafka and Pulsar are deliberately unauthenticated single-node development recipes. Kafka uses PLAINTEXT and Pulsar uses standalone mode, so do not expose either service remotely. Use `DB_PORT=9092 make db DB=kafka@4.3` for Kafka's usual port, and `DB_PORT=6650 PULSAR_WEB_PORT=8080 make db DB=pulsar@4.2` for Pulsar's usual ports.
Redis 3.0.7 and Redis 7.4.9 intentionally cover the compatibility endpoints. Redis 3 predates ACLs, RESP3, Streams, and many modern commands, while Redis 7 covers the current protocol and command set. Keeping 6.2 and 7.4 together would provide less compatibility coverage for this lab.
Versions of the same product intentionally share that default host port. Start them one at a time, or give one version another port when comparing versions concurrently:
Consul runs as a single-node development agent with `agent -dev -client=0.0.0.0 -ui` and no ACLs. Its HTTP API and UI, gRPC, and DNS host ports default to `10900`, `10901`, and `10902`; DNS is published over both TCP and UDP. Use the following command to match the usual local ports `8500`, `8502`, and `8600`:
```bash
DB_PORT=13307 make db DB=mysql@5.7
DB_PORT=8500 CONSUL_GRPC_PORT=8502 CONSUL_DNS_PORT=8600 make db DB=consul@2.0.2
```
Elasticsearch 6.8 runs as an unauthenticated single-node development cluster with machine-learning features disabled. Its HTTP API and transport host ports default to `11500` and `11501`; use the following command to match the upstream ports `9200` and `9300`:
```bash
DB_PORT=9200 ELASTICSEARCH_TRANSPORT_PORT=9300 make db DB=elasticsearch@6.8
```
Every checked-in recipe has a unique default host-port allocation, so versions and products can run concurrently. Use `DB_PORT` only when a local policy requires a different host port:
```bash
DB_PORT=10150 make db DB=mysql@5.7
make db DB=mysql@8.4
```
@@ -79,15 +93,17 @@ make db-list
make db DB=postgresql@17.4
make db-verify DB=postgresql@17.4
make db DB=rnacos@0.8
make db DB=consul@2.0.2
make db DB=elasticsearch@6.8
```
`make db` without parameters prints every currently supported copy-and-paste start command, plus optional parameters. With `DB=product@version`, it waits for the Compose health check and prints the DBX connection fields. `db-verify` also runs the recipe's smoke command inside the database container and checks its expected output. It is the preferred command before reporting a database-specific fix.
`make db` without parameters prints every currently supported copy-and-paste start command, plus optional parameters. With `DB=product@version`, it waits for the Compose health check and prints the DBX connection fields. For database types supported by DBX, it also prints a prefilled `dbx://connection/new` deep link. Copy the link into a browser or run `open '<link>'` to open the new-connection dialog in DBX Desktop. Install and open DBX Desktop once before use so the operating system registers the `dbx://` protocol. The deep link can contain a password, so do not save it in shared terminal history, logs, or tickets. Recipes without a compatible DBX connection type explicitly report that no deep link is available, including Consul, Nacos, Kafka, Pulsar, and r-nacos. `db-verify` also runs the recipe's smoke command inside the database container and checks its expected output. It is the preferred command before reporting a database-specific fix.
## Commands and variables
| Make target | Purpose |
| --- | --- |
| `make db-list` | List database versions, images, and supported platforms |
| `make db-list` | List database versions, container-to-host port mappings, images, and supported platforms |
| `make db DB=product@version` | Create, wait for, and print the connection fields |
| `make db-verify DB=product@version` | Start and run smoke checks |
| `make db-down DB=product@version` | Stop the environment while keeping its data volume |
+167 -8
View File
@@ -10,6 +10,35 @@ export const DEFAULT_RECIPES_ROOT = join(REPO_ROOT, 'deploy', 'database');
const DEFAULT_MAKEFILE_PATH = join(REPO_ROOT, 'Makefile');
const DEFAULT_PASSWORD = '123456';
const DEFAULT_DATABASE = 'dbx';
const DEFAULT_HOST_PORT_RANGES = {
mysql: [10100, 10199],
mariadb: [10200, 10299],
postgresql: [10300, 10399],
mongodb: [10400, 10499],
redis: [10500, 10599],
clickhouse: [10600, 10699],
etcd: [10700, 10799],
zookeeper: [10800, 10899],
consul: [10900, 10999],
nacos: [11000, 11099],
rnacos: [11100, 11199],
qdrant: [11200, 11299],
kafka: [11300, 11399],
pulsar: [11400, 11499],
elasticsearch: [11500, 11599],
};
const DBX_DEEP_LINK_TYPES = {
clickhouse: 'clickhouse',
elasticsearch: 'elasticsearch',
etcd: 'etcd',
mariadb: 'mariadb',
mongodb: 'mongodb',
mysql: 'mysql',
postgresql: 'postgres',
qdrant: 'qdrant',
redis: 'redis',
zookeeper: 'zookeeper',
};
export function discoverRecipes(root = DEFAULT_RECIPES_ROOT) {
if (!existsSync(root)) return [];
@@ -45,6 +74,22 @@ export function formatTable(headers, rows) {
return [headers, ...rows].map(renderRow).join('\n');
}
export function formatBorderedTable(headers, rows, groupByFirstColumn = false) {
const widths = headers.map((header, index) => Math.max(
header.length,
...rows.map((row) => String(row[index] ?? '').length),
));
const border = `+${widths.map((width) => '-'.repeat(width + 2)).join('+')}+`;
const renderRow = (row) => `| ${row.map((value, index) => String(value ?? '').padEnd(widths[index])).join(' | ')} |`;
const body = [];
for (const [index, row] of rows.entries()) {
body.push(renderRow(row));
if (groupByFirstColumn && index < rows.length - 1 && rows[index + 1][0] !== '') body.push(border);
}
return [border, renderRow(headers), border, ...body, border].join('\n');
}
export function discoverMakeTargets(makefilePath = DEFAULT_MAKEFILE_PATH) {
if (!existsSync(makefilePath)) return [];
const source = readFileSync(makefilePath, 'utf8').replace(/\\\r?\n/g, ' ');
@@ -119,18 +164,95 @@ export function expectedContainerName(recipe) {
return `dbx-${recipe.database}-${recipe.displayVersion}`.toLowerCase().replace(/[^a-z0-9_.-]+/g, '-');
}
export function defaultHostPortMappings(compose) {
const mappings = [];
const pattern = /^\s*-\s*["']?\$\{DB_BIND_ADDRESS:-127\.0\.0\.1\}:\$\{([A-Z][A-Z0-9_]*):-([0-9]+)\}:([0-9]+)(?:\/(tcp|udp))?["']?\s*$/;
for (const line of compose.split(/\r?\n/)) {
const match = line.match(pattern);
if (!match) continue;
mappings.push({ environment: match[1], hostPort: Number(match[2]), containerPort: Number(match[3]), protocol: match[4] || 'tcp' });
}
return mappings;
}
export function recipePortMappings(recipe) {
const composePath = join(recipe.directory, 'compose.yaml');
if (!existsSync(composePath)) return '';
const mappings = defaultHostPortMappings(readFileSync(composePath, 'utf8')).sort((left, right) => (
left.hostPort - right.hostPort
|| left.containerPort - right.containerPort
|| left.protocol.localeCompare(right.protocol)
));
return mappings.map((mapping) => {
const sharesPortWithAnotherProtocol = mappings.some((candidate) => (
candidate !== mapping
&& candidate.containerPort === mapping.containerPort
&& candidate.hostPort === mapping.hostPort
&& candidate.protocol !== mapping.protocol
));
const sourcePort = sharesPortWithAnotherProtocol
? `${mapping.containerPort}/${mapping.protocol}`
: mapping.containerPort;
return `${sourcePort} -> ${mapping.hostPort}`;
}).join(', ');
}
export function databaseListRows(recipes) {
let previousDatabase = null;
return recipes.map((recipe) => {
const database = recipe.database === previousDatabase ? '' : recipe.name;
previousDatabase = recipe.database;
return [database, recipe.displayVersion, recipePortMappings(recipe), recipe.image, recipe.platforms.join(',')];
});
}
function validateHostPorts(recipe) {
const errors = [];
const hostPorts = recipe.hostPorts;
if (!hostPorts || typeof hostPorts !== 'object' || Array.isArray(hostPorts)) return ['hostPorts must be a non-empty object'];
const entries = Object.entries(hostPorts);
if (entries.length === 0) return ['hostPorts must be a non-empty object'];
const [start, end] = DEFAULT_HOST_PORT_RANGES[recipe.database] ?? [];
if (!start) {
errors.push(`database ${recipe.database} has no allocated default host port range`);
}
for (const [environment, port] of entries) {
if (!/^[A-Z][A-Z0-9_]*$/.test(environment)) errors.push(`hostPorts key ${environment} must be an environment variable name`);
if (!Number.isInteger(port) || port < start || port > end) {
errors.push(`hostPorts.${environment} must be an integer in the ${start}-${end} range`);
}
}
if (new Set(entries.map(([, port]) => port)).size !== entries.length) errors.push('hostPorts must not reuse a port within a recipe');
if (recipe.connection?.port !== hostPorts.DB_PORT) errors.push('connection.port must match hostPorts.DB_PORT');
return errors;
}
export function validateHostPortAllocation(recipes) {
const errors = [];
const owners = new Map();
for (const recipe of recipes) {
for (const [environment, port] of Object.entries(recipe.hostPorts ?? {})) {
if (!Number.isInteger(port)) continue;
const owner = `${recipeSelector(recipe)} (${environment})`;
const existing = owners.get(port);
if (existing) errors.push(`host port ${port} is allocated to both ${existing} and ${owner}`);
else owners.set(port, owner);
}
}
return errors;
}
export function validateRecipe(recipe) {
const errors = [];
for (const field of ['database', 'version', 'displayVersion', 'service', 'connection', 'smoke', 'shell', 'defaultPort']) {
for (const field of ['database', 'version', 'displayVersion', 'service', 'connection', 'hostPorts', 'smoke', 'shell', 'defaultPort']) {
if (recipe[field] === undefined) errors.push(`missing ${field}`);
}
if (!recipe.image || !/^[^\s]+:[^\s]+$/.test(recipe.image)) errors.push('image must be a pinned image reference');
if (recipe.connection?.host !== '127.0.0.1') errors.push('connection.host must be 127.0.0.1');
if (!Number.isInteger(recipe.defaultPort) || recipe.defaultPort < 1 || recipe.defaultPort > 65534) {
errors.push('defaultPort must be an integer between 1 and 65534');
} else if (recipe.connection?.port !== recipe.defaultPort + 1) {
errors.push('connection.port must be defaultPort + 1');
}
errors.push(...validateHostPorts(recipe));
if (recipe.connection?.authentication === 'none') {
if (recipe.connection.password !== undefined) errors.push('unauthenticated recipes must not declare connection.password');
} else if (recipe.connection?.password !== DEFAULT_PASSWORD) errors.push(`connection.password must be ${DEFAULT_PASSWORD}`);
@@ -162,8 +284,18 @@ export function validateRecipe(recipe) {
errors.push('compose.yaml ports must default every mapping to 127.0.0.1 via DB_BIND_ADDRESS');
}
if (!serviceHasNamedVolume(compose, recipe.service)) errors.push('target service must mount a named volume');
if (!compose.includes(`\${DB_PORT:-${recipe.connection?.port}}:${recipe.defaultPort}`)) {
errors.push('compose.yaml must default DB_PORT to connection.port and target defaultPort');
const portMappings = defaultHostPortMappings(compose);
const mappedPorts = new Map();
for (const mapping of portMappings) {
const previousPort = mappedPorts.get(mapping.environment);
if (previousPort !== undefined && previousPort !== mapping.hostPort) {
errors.push(`compose.yaml uses conflicting defaults for ${mapping.environment}`);
}
mappedPorts.set(mapping.environment, mapping.hostPort);
}
const configuredPorts = Object.entries(recipe.hostPorts ?? {});
if (mappedPorts.size !== configuredPorts.length || configuredPorts.some(([environment, port]) => mappedPorts.get(environment) !== port)) {
errors.push('compose.yaml port defaults must match recipe hostPorts');
}
}
const initDirectory = join(recipe.directory, 'init');
@@ -289,6 +421,24 @@ export function expandSmokeCommand(command, recipe, environment = process.env) {
return command.map((value) => value.replace(/\$\{(DB_PASSWORD|DB_PORT)\}/g, (_, name) => values[name]));
}
export function dbxConnectionDeepLink(recipe, environment = process.env) {
const type = DBX_DEEP_LINK_TYPES[recipe.database];
if (!type) return null;
const { connection } = recipe;
const params = new URLSearchParams({
type,
name: `${recipe.name} ${recipe.version} (local)`,
host: connection.host,
port: environment.DB_PORT || String(connection.port),
database: String(connection.database),
});
if (connection.username) params.set('user', connection.username);
if (connection.password) params.set('password', environment.DB_PASSWORD || connection.password);
if (connection.urlParams) params.set('url_params', connection.urlParams);
return `dbx://connection/new?${params}`;
}
function ensureBootstrap(recipe) {
if (!recipe.bootstrap) return;
// Keep one-shot setup synchronous after Compose health checks so --wait cannot return before credentials exist.
@@ -310,6 +460,9 @@ function printConnection(recipe) {
if (process.env.DB_PASSWORD) connection.password = process.env.DB_PASSWORD;
console.log(`${recipe.name} (${recipe.version})`);
for (const [key, value] of Object.entries(connection)) console.log(`${key}: ${value}`);
const deepLink = dbxConnectionDeepLink(recipe);
if (deepLink) console.log(`DBX connection link: ${deepLink}`);
else console.log(`DBX connection link: unavailable (DBX has no compatible ${recipe.name} connection type)`);
if (recipe.notes) console.log(`notes: ${recipe.notes}`);
const warning = architectureWarning(recipe);
if (warning) console.warn(`warning: ${warning}`);
@@ -317,6 +470,11 @@ function printConnection(recipe) {
function checkRecipes(recipes) {
let failed = false;
const allocationErrors = validateHostPortAllocation(recipes);
if (allocationErrors.length > 0) {
failed = true;
for (const error of allocationErrors) console.error(`FAIL host port allocation: ${error}`);
}
for (const recipe of recipes) {
const errors = validateRecipe(recipe);
if (errors.length) {
@@ -368,9 +526,10 @@ export function main(argv = process.argv.slice(2)) {
const recipes = discoverRecipes();
if (command === 'list') {
console.log(formatTable(
['DATABASE', 'VERSION', 'IMAGE', 'PLATFORMS'],
recipes.map((recipe) => [recipe.database, recipe.displayVersion, recipe.image, recipe.platforms.join(',')]),
console.log(formatBorderedTable(
['DATABASE', 'VERSION', 'PORTS (CONTAINER -> HOST)', 'IMAGE', 'PLATFORMS'],
databaseListRows(recipes),
true,
));
return;
}
+106 -9
View File
@@ -4,19 +4,20 @@ import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { spawnSync } from 'node:child_process';
import { DEFAULT_RECIPES_ROOT, allPortMappingsDefaultToLoopback, architectureWarning, assertResetConfirmed, discoverMakeTargets, discoverRecipes, expandSmokeCommand, expectedContainerName, formatTable, parseDatabaseSelection, platformForArchitecture, recipeSelector, resolveRecipe, serviceHasNamedVolume, validateRecipe, validateRenderedCompose } from './database-env.mjs';
import { DEFAULT_RECIPES_ROOT, allPortMappingsDefaultToLoopback, architectureWarning, assertResetConfirmed, databaseListRows, dbxConnectionDeepLink, defaultHostPortMappings, discoverMakeTargets, discoverRecipes, expandSmokeCommand, expectedContainerName, formatBorderedTable, formatTable, parseDatabaseSelection, platformForArchitecture, recipeSelector, resolveRecipe, serviceHasNamedVolume, validateHostPortAllocation, validateRecipe, validateRenderedCompose } from './database-env.mjs';
const bashAvailable = spawnSync('bash', ['--version'], { stdio: 'ignore' }).status === 0;
const zshAvailable = spawnSync('zsh', ['--version'], { stdio: 'ignore' }).status === 0;
const dockerComposeAvailable = spawnSync('docker', ['compose', 'version'], { stdio: 'ignore' }).status === 0;
function fixture(database, version) {
const hostPort = { mysql: 10100, postgresql: 10300, redis: 10500 }[database] ?? 10100;
const root = mkdtempSync(join(tmpdir(), 'dbx-db-env-'));
const directory = join(root, database, version);
mkdirSync(join(directory, 'init'), { recursive: true });
writeFileSync(join(directory, 'init', '001-smoke.txt'), 'fixture initialization');
writeFileSync(join(directory, 'recipe.json'), JSON.stringify({ database, version, displayVersion: version, name: database, image: 'test:1', platforms: ['linux/amd64'], service: 'database', defaultPort: 1234, connection: { host: '127.0.0.1', port: 1235, password: '123456', database: database === 'redis' ? 0 : 'dbx' }, smoke: { steps: [{ name: 'smoke', command: ['true'], expect: 'true' }] }, shell: ['true'] }));
writeFileSync(join(directory, 'compose.yaml'), `services:\n database:\n image: test:1\n platform: linux/amd64\n container_name: dbx-${database}-${version}\n ports:\n - "\${DB_BIND_ADDRESS:-127.0.0.1}:\${DB_PORT:-1235}:1234"\n volumes:\n - data:/var/lib/database\n healthcheck:\n test: ["CMD", "true"]\nvolumes:\n data:\n`);
writeFileSync(join(directory, 'recipe.json'), JSON.stringify({ database, version, displayVersion: version, name: database, image: 'test:1', platforms: ['linux/amd64'], service: 'database', defaultPort: 1234, connection: { host: '127.0.0.1', port: hostPort, password: '123456', database: database === 'redis' ? 0 : 'dbx' }, hostPorts: { DB_PORT: hostPort }, smoke: { steps: [{ name: 'smoke', command: ['true'], expect: 'true' }] }, shell: ['true'] }));
writeFileSync(join(directory, 'compose.yaml'), `services:\n database:\n image: test:1\n platform: linux/amd64\n container_name: dbx-${database}-${version}\n ports:\n - "\${DB_BIND_ADDRESS:-127.0.0.1}:\${DB_PORT:-${hostPort}}:1234"\n volumes:\n - data:/var/lib/database\n healthcheck:\n test: ["CMD", "true"]\nvolumes:\n data:\n`);
return root;
}
@@ -59,6 +60,35 @@ test('formats lists as left-aligned tables with dynamic column widths', () => {
);
});
test('formats bordered tables with header and group separators', () => {
assert.equal(
formatBorderedTable(
['DATABASE', 'VERSION'],
[['MySQL', '5.7'], ['', '8.4'], ['PostgreSQL', '17.4']],
true,
),
'+------------+---------+\n'
+ '| DATABASE | VERSION |\n'
+ '+------------+---------+\n'
+ '| MySQL | 5.7 |\n'
+ '| | 8.4 |\n'
+ '+------------+---------+\n'
+ '| PostgreSQL | 17.4 |\n'
+ '+------------+---------+',
);
});
test('groups database versions and shows every container-to-host port mapping', () => {
const rows = databaseListRows(discoverRecipes());
const mysql57 = rows.find((row) => row[1] === '5.7');
const mysql84 = rows.find((row) => row[1] === '8.4');
const consul = rows.find((row) => row[0] === 'Consul');
assert.deepEqual(mysql57.slice(0, 3), ['MySQL', '5.7', '3306 -> 10100']);
assert.deepEqual(mysql84.slice(0, 3), ['', '8.4', '3306 -> 10101']);
assert.equal(consul[2], '8500 -> 10900, 8502 -> 10901, 8600/tcp -> 10902, 8600/udp -> 10902');
});
test('discovers all public Make targets for shell completion', () => {
const targets = discoverMakeTargets();
for (const target of ['help', 'build', 'test', 'docs', 'db', 'db-verify', 'db-completion']) {
@@ -140,6 +170,8 @@ test('uses the approved CNB mirror images and matching recipe versions', () => {
);
assert.deepEqual(recipes, {
'clickhouse@24.8': { version: '24.8.14.39', image: 'docker.cnb.cool/znb/images/clickhouse-server:24.8.14.39', platforms: ['linux/amd64', 'linux/arm64'] },
'consul@2.0.2': { version: '2.0.2', image: 'docker.cnb.cool/znb/images/consul:2.0.2', platforms: ['linux/amd64', 'linux/arm64'] },
'elasticsearch@6.8': { version: '6.8', image: 'docker.cnb.cool/znb/images/elasticsearch:6.8', platforms: ['linux/amd64', 'linux/arm64'] },
'etcd@3.7': { version: '3.7.0', image: 'docker.cnb.cool/znb/images/etcd:v3.7.0', platforms: ['linux/amd64', 'linux/arm64'] },
'kafka@4.3': { version: '4.3.1', image: 'docker.cnb.cool/znb/images/kafka:4.3.1', platforms: ['linux/amd64', 'linux/arm64'] },
'mariadb@10.11': { version: '10.11.11', image: 'docker.cnb.cool/znb/images/mariadb:10.11.11', platforms: ['linux/amd64', 'linux/arm64'] },
@@ -148,7 +180,7 @@ test('uses the approved CNB mirror images and matching recipe versions', () => {
'mysql@5.7': { version: '5.7.44', image: 'docker.cnb.cool/znb/images/mysql:5.7.44', platforms: ['linux/amd64', 'linux/arm64'] },
'mysql@8.4': { version: '8.4.6', image: 'docker.cnb.cool/znb/images/mysql:8.4.6', platforms: ['linux/amd64', 'linux/arm64'] },
'nacos@2.5': { version: '2.5.2', image: 'docker.cnb.cool/znb/images/nacos-server:v2.5.2', platforms: ['linux/amd64', 'linux/arm64'] },
'nacos@3.2': { version: '3.2.2', image: 'docker.cnb.cool/znb/images/nacos-server:v3.2.2', platforms: ['linux/amd64', 'linux/arm64'] },
'nacos@3.2': { version: '3.2.3', image: 'docker.cnb.cool/znb/images/nacos-server:v3.2.3', platforms: ['linux/amd64', 'linux/arm64'] },
'postgresql@14.23': { version: '14.23', image: 'docker.cnb.cool/znb/images/postgres:14.23', platforms: ['linux/amd64', 'linux/arm64'] },
'postgresql@17.4': { version: '17.4', image: 'docker.cnb.cool/znb/images/postgres:17.4', platforms: ['linux/amd64', 'linux/arm64'] },
'pulsar@4.2': { version: '4.2.3', image: 'docker.cnb.cool/znb/images/pulsar:4.2.3', platforms: ['linux/amd64', 'linux/arm64'] },
@@ -181,8 +213,8 @@ test('configures r-nacos with its admin account and console port', () => {
const recipe = discoverRecipes().find((item) => recipeSelector(item) === 'rnacos@0.8');
assert.equal(recipe.connection.username, 'admin');
assert.equal(recipe.connection.password, '123456');
assert.equal(recipe.connection.grpcPort, 9849);
assert.equal(recipe.connection.consolePort, 10849);
assert.equal(recipe.connection.grpcPort, 11101);
assert.equal(recipe.connection.consolePort, 11102);
const compose = readFileSync(join(recipe.directory, 'compose.yaml'), 'utf8');
assert.match(compose, /RNACOS_CONSOLE_PORT/);
@@ -257,6 +289,46 @@ test('smoke commands use password and port overrides without invoking a shell',
);
});
test('generates DBX deep links from effective recipe connection values', () => {
const recipe = discoverRecipes().find((item) => recipeSelector(item) === 'postgresql@17.4');
assert.ok(recipe);
const link = dbxConnectionDeepLink(recipe, { DB_PORT: '15433', DB_PASSWORD: 'p@ss & word' });
assert.ok(link);
const params = new URL(link).searchParams;
assert.equal(params.get('type'), 'postgres');
assert.equal(params.get('name'), 'PostgreSQL 17.4 (local)');
assert.equal(params.get('host'), '127.0.0.1');
assert.equal(params.get('port'), '15433');
assert.equal(params.get('user'), 'postgres');
assert.equal(params.get('password'), 'p@ss & word');
assert.equal(params.get('database'), 'dbx');
});
test('prints a DBX deep link only for compatible connection types', () => {
const repoRoot = join(DEFAULT_RECIPES_ROOT, '..', '..');
const postgres = spawnSync(process.execPath, ['scripts/database-env.mjs', 'info', 'postgresql', '17.4'], {
cwd: repoRoot,
encoding: 'utf8',
});
assert.equal(postgres.status, 0, postgres.stderr);
assert.match(postgres.stdout, /DBX connection link: dbx:\/\/connection\/new\?type=postgres&/);
const elasticsearch = spawnSync(process.execPath, ['scripts/database-env.mjs', 'info', 'elasticsearch', '6.8'], {
cwd: repoRoot,
encoding: 'utf8',
});
assert.equal(elasticsearch.status, 0, elasticsearch.stderr);
assert.match(elasticsearch.stdout, /DBX connection link: dbx:\/\/connection\/new\?type=elasticsearch&/);
const consul = spawnSync(process.execPath, ['scripts/database-env.mjs', 'info', 'consul', '2.0.2'], {
cwd: repoRoot,
encoding: 'utf8',
});
assert.equal(consul.status, 0, consul.stderr);
assert.match(consul.stdout, /DBX connection link: unavailable \(DBX has no compatible Consul connection type\)/);
});
test('MongoDB commands pass reserved-character passwords as a distinct argument', () => {
const password = 'p@ss/word#with?reserved&characters';
for (const recipe of discoverRecipes().filter((item) => item.database === 'mongodb')) {
@@ -355,10 +427,35 @@ test('allows recipes that explicitly declare no authentication', () => {
assert.deepEqual(validateRecipe(recipe), []);
});
test('requires every recipe port to be one greater than its database default port', () => {
test('requires the primary connection port to match the allocated DB_PORT', () => {
const [recipe] = discoverRecipes(fixture('postgresql', '17.4'));
recipe.connection.port = 5432;
assert.match(validateRecipe(recipe).join('; '), /connection.port must be defaultPort \+ 1/);
recipe.connection.port = 10301;
assert.match(validateRecipe(recipe).join('; '), /connection.port must match hostPorts.DB_PORT/);
});
test('requires Compose port defaults to match the declared host-port allocation', () => {
const [recipe] = discoverRecipes(fixture('postgresql', '17.4'));
const composePath = join(recipe.directory, 'compose.yaml');
writeFileSync(composePath, readFileSync(composePath, 'utf8').replace('DB_PORT:-10300', 'DB_PORT:-10301'));
assert.match(validateRecipe(recipe).join('; '), /compose.yaml port defaults must match recipe hostPorts/);
});
test('allocates unique default host ports across checked-in recipes', () => {
const recipes = discoverRecipes();
assert.deepEqual(validateHostPortAllocation(recipes), []);
const duplicate = structuredClone(recipes[0]);
duplicate.hostPorts = { DB_PORT: recipes[1].hostPorts.DB_PORT };
assert.match(validateHostPortAllocation([recipes[1], duplicate]).join('; '), /host port .* is allocated to both/);
});
test('parses every declared default host-port mapping, including shared UDP and TCP DNS', () => {
const consul = discoverRecipes().find((recipe) => recipeSelector(recipe) === 'consul@2.0.2');
assert.ok(consul);
const mappings = defaultHostPortMappings(readFileSync(join(consul.directory, 'compose.yaml'), 'utf8'));
assert.deepEqual(mappings.filter((mapping) => mapping.environment === 'CONSUL_DNS_PORT'), [
{ environment: 'CONSUL_DNS_PORT', hostPort: 10902, containerPort: 8600, protocol: 'tcp' },
{ environment: 'CONSUL_DNS_PORT', hostPort: 10902, containerPort: 8600, protocol: 'udp' },
]);
});
test('reports missing smoke structures without throwing a TypeError', () => {