diff --git a/deploy/database/etcd/3.7/compose.yaml b/deploy/database/etcd/3.7/compose.yaml new file mode 100644 index 000000000..0b3247d29 --- /dev/null +++ b/deploy/database/etcd/3.7/compose.yaml @@ -0,0 +1,31 @@ +services: + database: + image: docker.cnb.cool/znb/images/etcd:v3.7.0 + container_name: dbx-etcd-3.7 + command: + - /usr/local/bin/etcd + - --name=s1 + - --data-dir=/etcd-data + - --listen-client-urls=http://0.0.0.0:2379 + - --advertise-client-urls=http://0.0.0.0:2379 + - --listen-peer-urls=http://0.0.0.0:2380 + - --initial-advertise-peer-urls=http://0.0.0.0:2380 + - --initial-cluster=s1=http://0.0.0.0:2380 + - --initial-cluster-token=tkn + - --initial-cluster-state=new + - --log-level=info + - --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" + volumes: + - data:/etcd-data + healthcheck: + test: ["CMD", "/usr/local/bin/etcdctl", "--endpoints=http://127.0.0.1:2379", "endpoint", "health"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 10s +volumes: + data: diff --git a/deploy/database/etcd/3.7/init/README.md b/deploy/database/etcd/3.7/init/README.md new file mode 100644 index 000000000..a76751ff5 --- /dev/null +++ b/deploy/database/etcd/3.7/init/README.md @@ -0,0 +1,3 @@ +# etcd 3.7 initialization + +After the server is healthy, the recipe runner creates `root` with password `123456` (or `DB_PASSWORD`), grants it the `root` role, and enables etcd authentication. The named volume keeps the initialized authentication state. diff --git a/deploy/database/etcd/3.7/recipe.json b/deploy/database/etcd/3.7/recipe.json new file mode 100644 index 000000000..2d04c8d02 --- /dev/null +++ b/deploy/database/etcd/3.7/recipe.json @@ -0,0 +1,22 @@ +{ + "database": "etcd", + "name": "etcd", + "version": "3.7.0", + "displayVersion": "3.7", + "image": "docker.cnb.cool/znb/images/etcd:v3.7.0", + "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 }, + "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": [ + { "name": "create root user", "command": ["/usr/local/bin/etcdctl", "--endpoints=http://127.0.0.1:2379", "user", "add", "root:${DB_PASSWORD}"], "expect": "User root created" }, + { "name": "create root role", "command": ["/usr/local/bin/etcdctl", "--endpoints=http://127.0.0.1:2379", "role", "add", "root"], "expect": "Role root created" }, + { "name": "grant root role", "command": ["/usr/local/bin/etcdctl", "--endpoints=http://127.0.0.1:2379", "user", "grant-role", "root", "root"], "expect": "Role root is granted to user root" }, + { "name": "enable authentication", "command": ["/usr/local/bin/etcdctl", "--endpoints=http://127.0.0.1:2379", "auth", "enable"], "expect": "Authentication Enabled" } + ] + }, + "shell": ["/usr/local/bin/etcdctl", "--endpoints=http://127.0.0.1:2379", "--user", "root:${DB_PASSWORD}"], + "smoke": { "steps": [{ "name": "write authenticated smoke key", "command": ["/usr/local/bin/etcdctl", "--endpoints=http://127.0.0.1:2379", "--user", "root:${DB_PASSWORD}", "put", "dbx:smoke", "DBX smoke"], "expect": "OK" }, { "name": "read authenticated smoke key", "command": ["/usr/local/bin/etcdctl", "--endpoints=http://127.0.0.1:2379", "--user", "root:${DB_PASSWORD}", "get", "dbx:smoke"], "expect": "DBX smoke" }] } +} diff --git a/deploy/database/kafka/4.3/compose.yaml b/deploy/database/kafka/4.3/compose.yaml new file mode 100644 index 000000000..0964abea5 --- /dev/null +++ b/deploy/database/kafka/4.3/compose.yaml @@ -0,0 +1,30 @@ +services: + database: + image: docker.cnb.cool/znb/images/kafka:4.3.1 + container_name: dbx-kafka-4.3 + ports: + - "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-9093}: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_CONTROLLER_LISTENER_NAMES: CONTROLLER + KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,INTERNAL:PLAINTEXT + KAFKA_CONTROLLER_QUORUM_VOTERS: 1@database:9094 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_NUM_PARTITIONS: 3 + volumes: + - data:/var/lib/kafka/data + healthcheck: + test: ["CMD-SHELL", "/opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server database:9095 >/dev/null"] + interval: 5s + timeout: 10s + retries: 30 + start_period: 20s +volumes: + data: diff --git a/deploy/database/kafka/4.3/init/README.md b/deploy/database/kafka/4.3/init/README.md new file mode 100644 index 000000000..8b38aa513 --- /dev/null +++ b/deploy/database/kafka/4.3/init/README.md @@ -0,0 +1,3 @@ +# Kafka 4.3 smoke data + +Kafka runs in a single-node KRaft configuration. The `verify` command creates the `dbx-smoke` topic, publishes `DBX smoke`, and consumes it through the internal broker listener. diff --git a/deploy/database/kafka/4.3/recipe.json b/deploy/database/kafka/4.3/recipe.json new file mode 100644 index 000000000..fbc680c59 --- /dev/null +++ b/deploy/database/kafka/4.3/recipe.json @@ -0,0 +1,13 @@ +{ + "database": "kafka", + "name": "Kafka", + "version": "4.3.1", + "displayVersion": "4.3", + "image": "docker.cnb.cool/znb/images/kafka:4.3.1", + "platforms": ["linux/amd64", "linux/arm64"], + "service": "database", + "defaultPort": 9092, + "connection": { "host": "127.0.0.1", "port": 9093, "authentication": "none", "database": "dbx", "internalPort": 9095 }, + "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" }] } +} diff --git a/deploy/database/nacos/2.5/compose.yaml b/deploy/database/nacos/2.5/compose.yaml new file mode 100644 index 000000000..57fa7dfb4 --- /dev/null +++ b/deploy/database/nacos/2.5/compose.yaml @@ -0,0 +1,29 @@ +services: + database: + image: docker.cnb.cool/znb/images/nacos-server:v2.5.2 + container_name: dbx-nacos-2.5 + 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" + environment: + MODE: standalone + PREFER_HOST_MODE: hostname + NACOS_AUTH_SYSTEM_TYPE: nacos + NACOS_AUTH_ENABLE: "true" + NACOS_AUTH_TOKEN: "bXlDdXN0b21TZWNyZXRLZXlGb3JOYWNvc1YzMTBNb3JlVGhhbjMyQ2hhcnM=" + NACOS_AUTH_IDENTITY_KEY: dbx_nacos_identity + NACOS_AUTH_IDENTITY_VALUE: dbx_nacos_identity_value + JVM_XMS: 256m + JVM_XMX: 256m + JVM_XMN: 128m + volumes: + - data:/home/nacos/data + healthcheck: + test: ["CMD-SHELL", "curl --fail --silent http://localhost:8848/nacos/v1/console/health/readiness | grep -q OK"] + interval: 5s + timeout: 5s + retries: 60 + start_period: 20s +volumes: + data: diff --git a/deploy/database/nacos/2.5/init/README.md b/deploy/database/nacos/2.5/init/README.md new file mode 100644 index 000000000..53718b276 --- /dev/null +++ b/deploy/database/nacos/2.5/init/README.md @@ -0,0 +1,3 @@ +# Nacos 2.5 initialization + +After the Nacos service becomes healthy, the database environment command synchronously sets the first-run `nacos` administrator password to `123456` (or `DB_PASSWORD`) through the authentication API. Later starts verify those credentials without overwriting the account. The named data volume preserves the initialized account. diff --git a/deploy/database/nacos/2.5/recipe.json b/deploy/database/nacos/2.5/recipe.json new file mode 100644 index 000000000..24ee317cb --- /dev/null +++ b/deploy/database/nacos/2.5/recipe.json @@ -0,0 +1,17 @@ +{ + "database": "nacos", + "name": "Nacos", + "version": "2.5.2", + "displayVersion": "2.5", + "image": "docker.cnb.cool/znb/images/nacos-server:v2.5.2", + "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" }, + "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" }] + }, + "shell": ["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}"], + "smoke": { "steps": [{ "name": "log in with initialized administrator credentials", "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" }] } +} diff --git a/deploy/database/nacos/3.2/compose.yaml b/deploy/database/nacos/3.2/compose.yaml new file mode 100644 index 000000000..7a7235a75 --- /dev/null +++ b/deploy/database/nacos/3.2/compose.yaml @@ -0,0 +1,32 @@ +services: + database: + image: docker.cnb.cool/znb/images/nacos-server:v3.2.2 + container_name: dbx-nacos-3.2 + 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" + environment: + MODE: standalone + PREFER_HOST_MODE: hostname + NACOS_AUTH_SYSTEM_TYPE: nacos + NACOS_AUTH_ENABLE: "true" + NACOS_AUTH_ADMIN_ENABLE: "true" + NACOS_AUTH_CONSOLE_ENABLE: "true" + NACOS_AUTH_TOKEN: "bXlDdXN0b21TZWNyZXRLZXlGb3JOYWNvc1YzMTBNb3JlVGhhbjMyQ2hhcnM=" + NACOS_AUTH_IDENTITY_KEY: dbx_nacos_identity + NACOS_AUTH_IDENTITY_VALUE: dbx_nacos_identity_value + JVM_XMS: 256m + JVM_XMX: 256m + JVM_XMN: 128m + volumes: + - data:/home/nacos/data + healthcheck: + test: ["CMD-SHELL", "test \"$$(curl --silent --output /dev/null --write-out '%{http_code}' http://localhost:8848/nacos/v3/auth/user/admin)\" = 405"] + interval: 5s + timeout: 5s + retries: 60 + start_period: 20s +volumes: + data: diff --git a/deploy/database/nacos/3.2/init/README.md b/deploy/database/nacos/3.2/init/README.md new file mode 100644 index 000000000..c2bc8001b --- /dev/null +++ b/deploy/database/nacos/3.2/init/README.md @@ -0,0 +1,3 @@ +# Nacos 3.2 initialization + +After the Nacos service becomes healthy, the database environment command synchronously sets the first-run `nacos` administrator password to `123456` (or `DB_PASSWORD`) through the authentication API. Later starts verify those credentials without overwriting the account. The named data volume preserves the initialized account. diff --git a/deploy/database/nacos/3.2/recipe.json b/deploy/database/nacos/3.2/recipe.json new file mode 100644 index 000000000..f356fafab --- /dev/null +++ b/deploy/database/nacos/3.2/recipe.json @@ -0,0 +1,17 @@ +{ + "database": "nacos", + "name": "Nacos", + "version": "3.2.2", + "displayVersion": "3.2", + "image": "docker.cnb.cool/znb/images/nacos-server:v3.2.2", + "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 }, + "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" }] + }, + "shell": ["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}"], + "smoke": { "steps": [{ "name": "log in with initialized administrator credentials", "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" }] } +} diff --git a/deploy/database/pulsar/4.2/compose.yaml b/deploy/database/pulsar/4.2/compose.yaml new file mode 100644 index 000000000..0d95f3e0c --- /dev/null +++ b/deploy/database/pulsar/4.2/compose.yaml @@ -0,0 +1,20 @@ +services: + database: + image: docker.cnb.cool/znb/images/pulsar:4.2.3 + container_name: dbx-pulsar-4.2 + 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" + volumes: + - data:/pulsar/data + - conf:/pulsar/conf + healthcheck: + test: ["CMD-SHELL", "bin/pulsar-admin brokers healthcheck"] + interval: 10s + timeout: 10s + retries: 60 + start_period: 30s +volumes: + data: + conf: diff --git a/deploy/database/pulsar/4.2/init/README.md b/deploy/database/pulsar/4.2/init/README.md new file mode 100644 index 000000000..0477555c7 --- /dev/null +++ b/deploy/database/pulsar/4.2/init/README.md @@ -0,0 +1,3 @@ +# Pulsar 4.2 smoke data + +Pulsar standalone creates the `public/default` namespace automatically. The `verify` command publishes `DBX smoke` to `persistent://public/default/dbx-smoke`. diff --git a/deploy/database/pulsar/4.2/recipe.json b/deploy/database/pulsar/4.2/recipe.json new file mode 100644 index 000000000..8cbbd592d --- /dev/null +++ b/deploy/database/pulsar/4.2/recipe.json @@ -0,0 +1,13 @@ +{ + "database": "pulsar", + "name": "Pulsar", + "version": "4.2.3", + "displayVersion": "4.2", + "image": "docker.cnb.cool/znb/images/pulsar:4.2.3", + "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 }, + "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" }] } +} diff --git a/deploy/database/qdrant/1.8/compose.yaml b/deploy/database/qdrant/1.8/compose.yaml new file mode 100644 index 000000000..88131c2f9 --- /dev/null +++ b/deploy/database/qdrant/1.8/compose.yaml @@ -0,0 +1,19 @@ +services: + database: + image: docker.cnb.cool/znb/images/qdrant:v1.8.3 + container_name: dbx-qdrant-1.8 + ports: + - "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-6334}:6333" + - "${DB_BIND_ADDRESS:-127.0.0.1}:${QDRANT_GRPC_PORT:-6335}:6334" + environment: + QDRANT__SERVICE__API_KEY: "${DB_PASSWORD:-123456}" + volumes: + - data:/qdrant/storage + healthcheck: + test: ["CMD-SHELL", "bash -c '/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" }] } +} diff --git a/deploy/database/zookeeper/3.9/compose.yaml b/deploy/database/zookeeper/3.9/compose.yaml new file mode 100644 index 000000000..ba8df7ebd --- /dev/null +++ b/deploy/database/zookeeper/3.9/compose.yaml @@ -0,0 +1,18 @@ +services: + database: + image: docker.cnb.cool/znb/images/zookeeper:3.9.5 + container_name: dbx-zookeeper-3.9 + ports: + - "${DB_BIND_ADDRESS:-127.0.0.1}:${DB_PORT:-2182}:2181" + volumes: + - data:/data + - datalog:/datalog + healthcheck: + test: ["CMD-SHELL", "zkServer.sh status | grep -q 'Mode: standalone'"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 15s +volumes: + data: + datalog: diff --git a/deploy/database/zookeeper/3.9/init/README.md b/deploy/database/zookeeper/3.9/init/README.md new file mode 100644 index 000000000..23aef802c --- /dev/null +++ b/deploy/database/zookeeper/3.9/init/README.md @@ -0,0 +1,3 @@ +# ZooKeeper 3.9 initialization + +The recipe runner creates `/dbx` with a Digest ACL for `root` and password `123456` (or `DB_PASSWORD`). ZooKeeper does not have a global username/password login switch; clients must authenticate with `addauth digest root:` before accessing this protected node. diff --git a/deploy/database/zookeeper/3.9/recipe.json b/deploy/database/zookeeper/3.9/recipe.json new file mode 100644 index 000000000..238f94c71 --- /dev/null +++ b/deploy/database/zookeeper/3.9/recipe.json @@ -0,0 +1,17 @@ +{ + "database": "zookeeper", + "name": "ZooKeeper", + "version": "3.9.5", + "displayVersion": "3.9", + "image": "docker.cnb.cool/znb/images/zookeeper:3.9.5", + "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" }, + "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" }] + }, + "shell": ["zkCli.sh", "-server", "127.0.0.1:2181"], + "smoke": { "steps": [{ "name": "reject anonymous access to protected node", "command": ["sh", "-ec", "printf 'get /dbx\\nquit\\n' | zkCli.sh -server 127.0.0.1:2181 || true"], "expect": "Insufficient permission" }, { "name": "read protected node with Digest credentials", "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" }] } +} diff --git a/docs/content/docs/database-lab.cn.mdx b/docs/content/docs/database-lab.cn.mdx index 62f2b60d9..83b3a21b2 100644 --- a/docs/content/docs/database-lab.cn.mdx +++ b/docs/content/docs/database-lab.cn.mdx @@ -12,18 +12,47 @@ description: 为 DBX 开发与验证启动可重复创建的本地数据库实 | 数据库 | 版本目录 | Docker Hub 官方镜像 | 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` | +| ZooKeeper | `zookeeper/3.9` | `zookeeper:3.9.5` | `docker.cnb.cool/znb/images/zookeeper:3.9.5` | `2182` | `dbx-zookeeper-3.9` | 所有网络数据库的默认宿主端口都比标准端口大 `1`,并且该约定由配方校验器强制检查。默认密码统一为 `123456`,默认数据库名为 `dbx`。Redis 不支持命名数据库,因此使用 DB 0,并以 `dbx:` 作为冒烟键前缀。Compose 默认使用 CNB 镜像列;Docker Hub 列保留官方上游地址,便于核对来源或直接拉取。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`。 + +```bash +DB_PORT=8818 NACOS_CONSOLE_PORT=8010 NACOS_GRPC_PORT=9818 NACOS_RAFT_PORT=9819 make db DB=nacos@3.2 +``` + +etcd 会创建密码为 `123456` 的 `root` 用户、授予 root 角色并开启认证。默认客户端和 peer 主机端口为 `2380`、`2381`。如需使用上游 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 命令中的端口,可执行: + +```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 模型保护单个节点,并不提供全局登录开关。 + +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 所提供的兼容性覆盖更小。 同一数据库的不同版本默认会共用宿主机端口。通常一次启动一个版本;需要并行对比时,为其中一个版本指定其他端口: diff --git a/docs/content/docs/database-lab.mdx b/docs/content/docs/database-lab.mdx index 2bb828773..cf30f4746 100644 --- a/docs/content/docs/database-lab.mdx +++ b/docs/content/docs/database-lab.mdx @@ -12,18 +12,47 @@ The Database Test Lab provides small, pinned Docker Compose environments for tes | Database | Version directory | Official Docker Hub 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` | +| ZooKeeper | `zookeeper/3.9` | `zookeeper:3.9.5` | `docker.cnb.cool/znb/images/zookeeper:3.9.5` | `2182` | `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. +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. + +```bash +DB_PORT=8818 NACOS_CONSOLE_PORT=8010 NACOS_GRPC_PORT=9818 NACOS_RAFT_PORT=9819 make db DB=nacos@3.2 +``` + +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: + +```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: + +```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. + +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: diff --git a/scripts/database-env.mjs b/scripts/database-env.mjs index e4c0ca5e2..a92a1f693 100644 --- a/scripts/database-env.mjs +++ b/scripts/database-env.mjs @@ -32,6 +32,19 @@ export function recipeSelector(recipe) { return `${recipe.database}@${recipe.displayVersion}`; } +export function formatTable(headers, rows) { + const widths = headers.map((header, index) => Math.max( + header.length, + ...rows.map((row) => String(row[index] ?? '').length), + )); + const renderRow = (row) => row.map((value, index) => { + const text = String(value ?? ''); + return index === row.length - 1 ? text : text.padEnd(widths[index]); + }).join(' '); + + return [headers, ...rows].map(renderRow).join('\n'); +} + export function discoverMakeTargets(makefilePath = DEFAULT_MAKEFILE_PATH) { if (!existsSync(makefilePath)) return []; const source = readFileSync(makefilePath, 'utf8').replace(/\\\r?\n/g, ' '); @@ -118,7 +131,9 @@ export function validateRecipe(recipe) { } else if (recipe.connection?.port !== recipe.defaultPort + 1) { errors.push('connection.port must be defaultPort + 1'); } - if (recipe.connection?.password !== DEFAULT_PASSWORD) errors.push(`connection.password must be ${DEFAULT_PASSWORD}`); + 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}`); if (recipe.database === 'redis' ? recipe.connection?.database !== 0 : recipe.connection?.database !== DEFAULT_DATABASE) { errors.push(`connection.database must be ${recipe.database === 'redis' ? '0 for Redis' : DEFAULT_DATABASE}`); } @@ -159,6 +174,17 @@ export function validateRecipe(recipe) { if (typeof step.expect !== 'string' || step.expect.length === 0) errors.push('every smoke step needs an expected output'); } if (!Array.isArray(recipe.shell) || recipe.shell.length === 0) errors.push('shell must be a non-empty command array'); + if (recipe.bootstrap !== undefined) { + if (!recipe.bootstrap || typeof recipe.bootstrap !== 'object') errors.push('bootstrap must be an object'); + if (!recipe.bootstrap?.check || !Array.isArray(recipe.bootstrap.check.command) || recipe.bootstrap.check.command.length === 0 || typeof recipe.bootstrap.check.expect !== 'string' || recipe.bootstrap.check.expect.length === 0) { + errors.push('bootstrap.check needs a command and expected output'); + } + if (!Array.isArray(recipe.bootstrap?.steps) || recipe.bootstrap.steps.length === 0) errors.push('bootstrap.steps must not be empty'); + for (const step of recipe.bootstrap?.steps ?? []) { + if (!step.name || !Array.isArray(step.command) || step.command.length === 0) errors.push('every bootstrap step needs a name and command'); + if (typeof step.expect !== 'string' || step.expect.length === 0) errors.push('every bootstrap step needs an expected output'); + } + } return errors; } @@ -249,6 +275,12 @@ function runCompose(recipe, args, options) { return run('docker', composeArgs(recipe, ...args), options); } +function tryRunCompose(recipe, args) { + const result = spawnSync('docker', composeArgs(recipe, ...args), { cwd: REPO_ROOT, env: process.env, encoding: 'utf8', stdio: 'pipe' }); + if (result.error) throw result.error; + return { ok: result.status === 0, output: `${result.stdout ?? ''}${result.stderr ?? ''}` }; +} + export function expandSmokeCommand(command, recipe, environment = process.env) { const values = { DB_PASSWORD: environment.DB_PASSWORD || recipe.connection.password, @@ -257,6 +289,21 @@ export function expandSmokeCommand(command, recipe, environment = process.env) { return command.map((value) => value.replace(/\$\{(DB_PASSWORD|DB_PORT)\}/g, (_, name) => values[name])); } +function ensureBootstrap(recipe) { + if (!recipe.bootstrap) return; + // Keep one-shot setup synchronous after Compose health checks so --wait cannot return before credentials exist. + const checkCommand = expandSmokeCommand(recipe.bootstrap.check.command, recipe); + const check = tryRunCompose(recipe, ['exec', '-T', recipe.service, ...checkCommand]); + if (check.ok && check.output.includes(recipe.bootstrap.check.expect)) return; + + for (const step of recipe.bootstrap.steps) { + const command = expandSmokeCommand(step.command, recipe); + const output = runCompose(recipe, ['exec', '-T', recipe.service, ...command], { capture: true }); + if (!output.includes(step.expect)) throw new Error(`Bootstrap check did not contain expected text: ${step.expect}\n${output}`); + console.log(`OK ${step.name}`); + } +} + function printConnection(recipe) { const connection = { ...recipe.connection }; if (process.env.DB_PORT) connection.port = Number(process.env.DB_PORT); @@ -321,8 +368,10 @@ export function main(argv = process.argv.slice(2)) { const recipes = discoverRecipes(); if (command === 'list') { - console.log('DATABASE\tVERSION\tIMAGE\tPLATFORMS'); - for (const recipe of recipes) console.log(`${recipe.database}\t${recipe.displayVersion}\t${recipe.image}\t${recipe.platforms.join(',')}`); + console.log(formatTable( + ['DATABASE', 'VERSION', 'IMAGE', 'PLATFORMS'], + recipes.map((recipe) => [recipe.database, recipe.displayVersion, recipe.image, recipe.platforms.join(',')]), + )); return; } if (command === 'quick-start') return printQuickStart(recipes); @@ -349,6 +398,7 @@ export function main(argv = process.argv.slice(2)) { case 'start': case 'up': runCompose(recipe, ['up', '-d', '--wait']); + ensureBootstrap(recipe); printConnection(recipe); break; case 'status': @@ -362,6 +412,7 @@ export function main(argv = process.argv.slice(2)) { break; case 'verify': runCompose(recipe, ['up', '-d', '--wait']); + ensureBootstrap(recipe); for (const step of recipe.smoke.steps) { const command = expandSmokeCommand(step.command, recipe); const output = runCompose(recipe, ['exec', '-T', recipe.service, ...command], { capture: true }); diff --git a/scripts/database-env.test.mjs b/scripts/database-env.test.mjs index 0debf4673..b01598dc6 100644 --- a/scripts/database-env.test.mjs +++ b/scripts/database-env.test.mjs @@ -4,7 +4,7 @@ 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, parseDatabaseSelection, platformForArchitecture, recipeSelector, resolveRecipe, serviceHasNamedVolume, validateRecipe, validateRenderedCompose } from './database-env.mjs'; +import { DEFAULT_RECIPES_ROOT, allPortMappingsDefaultToLoopback, architectureWarning, assertResetConfirmed, discoverMakeTargets, discoverRecipes, expandSmokeCommand, expectedContainerName, formatTable, parseDatabaseSelection, platformForArchitecture, recipeSelector, resolveRecipe, serviceHasNamedVolume, validateRecipe, validateRenderedCompose } from './database-env.mjs'; const bashAvailable = spawnSync('bash', ['--version'], { stdio: 'ignore' }).status === 0; const zshAvailable = spawnSync('zsh', ['--version'], { stdio: 'ignore' }).status === 0; @@ -44,6 +44,21 @@ test('formats a recipe as a copyable compact selector', () => { assert.equal(recipeSelector({ database: 'mysql', displayVersion: '8.4' }), 'mysql@8.4'); }); +test('formats lists as left-aligned tables with dynamic column widths', () => { + assert.equal( + formatTable( + ['DATABASE', 'VERSION', 'IMAGE'], + [ + ['etcd', '3.7', 'etcd:v3.7.0'], + ['clickhouse', '24.8', 'clickhouse-server:24.8.14.39'], + ], + ), + 'DATABASE VERSION IMAGE\n' + + 'etcd 3.7 etcd:v3.7.0\n' + + 'clickhouse 24.8 clickhouse-server:24.8.14.39', + ); +}); + 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']) { @@ -125,19 +140,78 @@ 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'] }, + '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'] }, 'mongodb@5.0': { version: '5.0.5', image: 'docker.cnb.cool/znb/images/mongo:5.0.5', platforms: ['linux/amd64', 'linux/arm64'] }, 'mongodb@8.2': { version: '8.2.3', image: 'docker.cnb.cool/znb/images/mongo:8.2.3-noble', platforms: ['linux/amd64', 'linux/arm64'] }, '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'] }, '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'] }, + 'qdrant@1.8': { version: '1.8.3', image: 'docker.cnb.cool/znb/images/qdrant:v1.8.3', platforms: ['linux/amd64', 'linux/arm64'] }, 'redis@3.0.7': { version: '3.0.7', image: 'docker.cnb.cool/znb/images/redis:3.0.7-alpine', platforms: ['linux/amd64'] }, 'redis@7.4': { version: '7.4.9', image: 'docker.cnb.cool/znb/images/redis:7.4.9-alpine', platforms: ['linux/amd64', 'linux/arm64'] }, + 'zookeeper@3.9': { version: '3.9.5', image: 'docker.cnb.cool/znb/images/zookeeper:3.9.5', platforms: ['linux/amd64', 'linux/arm64'] }, }); assert.equal(discoverRecipes().find((recipe) => recipeSelector(recipe) === 'redis@3.0.7').connection.username, undefined); }); +test('initializes both Nacos versions with the shared administrator credentials', () => { + for (const recipe of discoverRecipes().filter((item) => item.database === 'nacos')) { + assert.equal(recipe.connection.username, 'nacos'); + assert.equal(recipe.connection.password, '123456'); + assert.equal(recipe.connection.namespace, 'public'); + assert.equal(recipe.bootstrap.check.expect, 'accessToken'); + assert.equal(recipe.bootstrap.steps.length, 1); + assert.match(recipe.bootstrap.steps[0].command.join(' '), /administrator initialized/); + + const compose = readFileSync(join(recipe.directory, 'compose.yaml'), 'utf8'); + assert.doesNotMatch(compose, /^ initialize:/m); + assert.doesNotMatch(compose, /tail -f \/dev\/null/); + assert.deepEqual(validateRecipe(recipe), []); + } +}); + +test('configures Qdrant with the shared API key', () => { + const recipe = discoverRecipes().find((item) => recipeSelector(item) === 'qdrant@1.8'); + assert.equal(recipe.connection.username, ''); + assert.equal(recipe.connection.password, '123456'); + assert.equal(recipe.connection.apiKey, undefined); +}); + +test('bootstraps etcd with the shared root credentials', () => { + const recipe = discoverRecipes().find((item) => recipeSelector(item) === 'etcd@3.7'); + assert.equal(recipe.connection.username, 'root'); + assert.equal(recipe.connection.password, '123456'); + assert.ok(recipe.bootstrap); +}); + +test('bootstraps ZooKeeper with the shared Digest credentials', () => { + const recipe = discoverRecipes().find((item) => recipeSelector(item) === 'zookeeper@3.9'); + assert.equal(recipe.connection.username, 'root'); + assert.equal(recipe.connection.password, '123456'); + assert.equal(recipe.connection.authScheme, 'digest'); +}); + +test('keeps existing authenticated bootstrap recipes valid', () => { + for (const selector of ['etcd@3.7', 'zookeeper@3.9']) { + const recipe = discoverRecipes().find((item) => recipeSelector(item) === selector); + assert.deepEqual(validateRecipe(recipe), []); + } +}); + +test('marks the unauthenticated Kafka and Pulsar development recipes explicitly', () => { + for (const selector of ['kafka@4.3', 'pulsar@4.2']) { + const recipe = discoverRecipes().find((item) => recipeSelector(item) === selector); + assert.equal(recipe.connection.authentication, 'none'); + assert.equal(recipe.connection.password, undefined); + } +}); + test('start without DB prints every copyable database command', () => { const repoRoot = join(DEFAULT_RECIPES_ROOT, '..', '..'); const result = spawnSync(process.execPath, ['scripts/database-env.mjs', 'start'], { @@ -260,6 +334,13 @@ test('requires the shared default password and database name', () => { assert.match(validateRecipe(recipe).join('; '), /connection.database must be dbx/); }); +test('allows recipes that explicitly declare no authentication', () => { + const [recipe] = discoverRecipes(fixture('postgresql', '17.4')); + recipe.connection.authentication = 'none'; + delete recipe.connection.password; + assert.deepEqual(validateRecipe(recipe), []); +}); + test('requires every recipe port to be one greater than its database default port', () => { const [recipe] = discoverRecipes(fixture('postgresql', '17.4')); recipe.connection.port = 5432;