From 10174681a54138a4d67089e02829c137d6b2e79d Mon Sep 17 00:00:00 2001 From: z Date: Tue, 5 May 2026 19:51:04 +0800 Subject: [PATCH] add git-based chatapp testbox deploy --- testbox/.env.example | 243 +++++++++++++++++ testbox/.gitignore | 4 + testbox/Caddyfile | 17 ++ testbox/README.md | 98 +++++++ testbox/config/rocketmq/broker.conf | 11 + testbox/docker-compose.apps.yml | 302 ++++++++++++++++++++++ testbox/docker-compose.infra.yml | 153 +++++++++++ testbox/docker/chatapp3-admin.Dockerfile | 34 +++ testbox/mysql/baseline/.gitkeep | 0 testbox/mysql/baseline/README.md | 8 + testbox/mysql/init/00-create-db.sql | 1 + testbox/scripts/apply-mysql-baseline.sh | 70 +++++ testbox/scripts/bootstrap-server.sh | 47 ++++ testbox/scripts/build-images.sh | 43 +++ testbox/scripts/create-rocketmq-topics.sh | 19 ++ testbox/scripts/deploy-all.sh | 30 +++ testbox/scripts/deploy-from-git-remote.sh | 100 +++++++ testbox/scripts/deploy-from-git.sh | 64 +++++ testbox/scripts/env-value.py | 31 +++ testbox/scripts/import-nacos-configs.py | 108 ++++++++ testbox/scripts/merge-env-defaults.sh | 32 +++ testbox/scripts/sync-to-testbox.sh | 10 + 22 files changed, 1425 insertions(+) create mode 100644 testbox/.env.example create mode 100644 testbox/.gitignore create mode 100644 testbox/Caddyfile create mode 100644 testbox/README.md create mode 100644 testbox/config/rocketmq/broker.conf create mode 100644 testbox/docker-compose.apps.yml create mode 100644 testbox/docker-compose.infra.yml create mode 100644 testbox/docker/chatapp3-admin.Dockerfile create mode 100644 testbox/mysql/baseline/.gitkeep create mode 100644 testbox/mysql/baseline/README.md create mode 100644 testbox/mysql/init/00-create-db.sql create mode 100755 testbox/scripts/apply-mysql-baseline.sh create mode 100755 testbox/scripts/bootstrap-server.sh create mode 100755 testbox/scripts/build-images.sh create mode 100755 testbox/scripts/create-rocketmq-topics.sh create mode 100755 testbox/scripts/deploy-all.sh create mode 100755 testbox/scripts/deploy-from-git-remote.sh create mode 100755 testbox/scripts/deploy-from-git.sh create mode 100755 testbox/scripts/env-value.py create mode 100755 testbox/scripts/import-nacos-configs.py create mode 100755 testbox/scripts/merge-env-defaults.sh create mode 100755 testbox/scripts/sync-to-testbox.sh diff --git a/testbox/.env.example b/testbox/.env.example new file mode 100644 index 0000000..fb16aff --- /dev/null +++ b/testbox/.env.example @@ -0,0 +1,243 @@ +# Host and public entry +TESTBOX_DOMAIN=jvapi-test.haiyihy.com +TESTBOX_PUBLIC_BASE_URL=https://jvapi-test.haiyihy.com +TESTBOX_ADMIN_DOMAIN=yumi-test-admi.haiyihy.com +TESTBOX_ADMIN_PUBLIC_BASE_URL=https://yumi-test-admi.haiyihy.com +TESTBOX_ADMIN_API_BASE_URL=https://yumi-test-admi.haiyihy.com/console +TESTBOX_ADMIN_OSS_URL= + +# Infra credentials. Change these before first production-like test. +MYSQL_DATABASE=likei +MYSQL_USER=likei +MYSQL_PASSWORD=change-me-likei +MYSQL_ROOT_PASSWORD=change-me-root +REDIS_PASSWORD= +NACOS_USERNAME=nacos +NACOS_PASSWORD=nacos +NACOS_NAMESPACE_ID=RedCircleServiceTest +NACOS_NAMESPACE_NAME=RedCircleServiceTest + +# Java runtime. Jar is built with Maven profile "test" for Nacos namespace, +# but runtime profile stays prod so existing profile groups still load. +SERVER_PROFILE_ACTIVE=prod +JAVA_XMS=128m +JAVA_XMX_SMALL=384m +JAVA_XMX_MEDIUM=512m +JAVA_METASPACE=192m + +# Nacos-resolved placeholders used by chatapp3-java/nacos_config. +LIKEI_MYSQL_JDBC_URL=jdbc:p6spy:mysql://mysql:3306/likei?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false +LIKEI_MYSQL_USERNAME=likei +LIKEI_MYSQL_PASSWORD=change-me-likei +LIKEI_REDIS_HOST=redis +LIKEI_REDIS_PORT=6379 +LIKEI_REDIS_DATABASE=0 +LIKEI_MONGO_URI=mongodb://mongo:27017/tarab_all + +# RocketMQ is deployed locally. Keep Java producer disabled first so service +# startup is stable; flip to false when you want to test MQ paths. +LIKEI_ROCKETMQ_ACCESS_KEY=test +LIKEI_ROCKETMQ_SECRET_KEY=test +LIKEI_ROCKETMQ_ENDPOINT=rocketmq-proxy:8081 +LIKEI_ROCKETMQ_LOCAL_DISABLE=true + +# Payment provider placeholders used by rc-service-order. These test defaults +# only keep the service bootable; replace them when testing real payment flows. +LIKEI_PAY_WEB_PAY_BASE_URL=https://jvapi-test.haiyihy.com +LIKEI_PAY_WEB_PAY_RESULT_PAGE=/pay/result +LIKEI_PAY_WEB_PAY_RECHARGE_PAGE=/pay/recharge +LIKEI_PAY_WEB_PAY_COLLECTION_PAGE=/pay/collection +LIKEI_GOOGLE_PAY_GLOBAL_SERVER_ACCOUNT=test +LIKEI_GOOGLE_PAY_GLOBAL_PATCH_JSON=payfile/likei_google_pay.json +LIKEI_GOOGLE_PAY_TAXAB_SERVER_ACCOUNT=test +LIKEI_GOOGLE_PAY_TAXAB_PUBLIC_KEY=test +LIKEI_GOOGLE_PAY_TAXAB_PATCH_JSON=payfile/likei_google_pay.json +LIKEI_PAYPAL_MERCHANT_ID=test +LIKEI_PAYPAL_SECRET_KEY=test +LIKEI_PAYPAL_DEV_MERCHANT_ID=test +LIKEI_PAYPAL_DEV_SECRET_KEY=test +LIKEI_HUAWEI_GRANT_TYPE=client_credentials +LIKEI_HUAWEI_CLIENT_SECRET=test +LIKEI_HUAWEI_CLIENT_ID=test +LIKEI_HUAWEI_OAUTH_LOGIN_URL=https://oauth-login.cloud.huawei.com/oauth2/v3/token +LIKEI_HUAWEI_IAP_PUBLIC_KEY=test +LIKEI_PAYERMAX_SECRET_KEY=test +LIKEI_PAYERMAX_MERCHANT_ID=test +LIKEI_PAYERMAX_RSA_PRIVATE_KEY=test +LIKEI_PAYERMAX_RSA_PUBLIC_KEY=test +LIKEI_PAYERMAX_PLATFORM_RSA_PUBLIC_KEY=test +LIKEI_PAYERMAX_APP_ID=test +LIKEI_PAYERMAX_GATEWAY_BASE_URL=https://pay-gate-uat.payermax.com +LIKEI_PAYERMAX_ENABLE_REAL_TIME_EXCHANGE_RATE=false +LIKEI_AIRWALLEX_CLIENT_ID=test +LIKEI_AIRWALLEX_API_KEY=test +LIKEI_AIRWALLEX_BASE_URL=https://api-demo.airwallex.com +LIKEI_PAYNICORN_APP_KEY=test +LIKEI_PAYNICORN_MERCHANT_SECRET=test +LIKEI_CLIPSPAY_MERCHANT_NO=test +LIKEI_CLIPSPAY_MD5_SECRET_KEY=test +LIKEI_CLIPSPAY_TERMINAL_NO=test +LIKEI_CLIPSPAY_TERMINAL_KEY=test +LIKEI_CLIPSPAY_NOTIFY_URL=https://jvapi-test.haiyihy.com/play-server-notice/clipspay +LIKEI_CLIPSPAY_PLACE_ORDER_API=https://example.invalid +LIKEI_CLIPSPAY_REDIRECT_RESULT=https://jvapi-test.haiyihy.com/pay/result +LIKEI_ORDER_TELEGRAM_BOT_TOKEN=test +LIKEI_ORDER_TELEGRAM_WEBHOOK_URL=https://jvapi-test.haiyihy.com + +# Generic Java placeholder defaults for single-node smoke testing. +LIKEI_FRAMEWORK_NACOS_STOP_KEY=test +GAME_INTERNAL_CALLBACK_SECRET=test-game-callback-secret +GAME_LUCKY_GIFT_GO_URL=http://golang:2900 +LUCKY_GIFT_GO_ENABLED=false +LUCKY_GIFT_GO_TIMEOUT_MILLIS=3000 +LUCKY_GIFT_API_ENABLED=false +LUCKY_GIFT_API_DEFAULT_URL=https://example.invalid +LUCKY_GIFT_API_CONFIG_1_STANDARD_ID=1 +LUCKY_GIFT_API_CONFIG_1_URL=https://example.invalid +LUCKY_GIFT_API_CONFIG_2_STANDARD_ID=2 +LUCKY_GIFT_API_CONFIG_2_URL=https://example.invalid +INVITE_CAMPAIGN_GO_ENABLED=false +INVITE_CAMPAIGN_GO_TIMEOUT_MILLIS=3000 +INVITE_CAMPAIGN_GO_URL=http://golang:2900 +LIKEI_WALLET_MYSQL_JDBC_URL=jdbc:p6spy:mysql://mysql:3306/likei?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false +LIKEI_PAY_FACTORY_CONFIG_CACHE_TTL_SECONDS=300 +LIKEI_RTC_APP_ID=test +LIKEI_RTC_AUTH_KEY=test +LIKEI_RTC_CERTIFICATE=test +LIKEI_RTC_KILL_ROOM_URL=https://example.invalid +LIKEI_AGORA_APP_CERTIFICATE=test +LIKEI_AGORA_AUTH_KEY=test +LIKEI_AGORA_TOKEN_EXPIRE_SECOND=3600 +LIKEI_AGORA_PRIVILEGE_EXPIRE_SECOND=3600 +LIKEI_IM_APP_ID=1400000000 +LIKEI_IM_KEY=test +LIKEI_IM_IDENTIFIER=administrator +LIKEI_IM_BASE_ENDPOINT=https://console.tim.qq.com +LIKEI_IM_NOTICE_ACCOUNT=notice +LIKEI_IM_NEWSLETTER_ACCOUNT=newsletter +LIKEI_IM_BROADCAST_GROUP_LOTFUN=test-lotfun +LIKEI_IM_BROADCAST_GROUP_LIKEI=test-likei +LIKEI_TRTC_SECRET_ID=test +LIKEI_TRTC_SECRET_KEY=test +LIKEI_TRTC_ENDPOINT=https://trtc.tencentcloudapi.com +LIKEI_TRTC_SDK_APP_ID=1400000000 +LIKEI_TENCENT_COS_ENABLED=false +LIKEI_TENCENT_COS_SECRET_ID=test +LIKEI_TENCENT_COS_SECRET_KEY=test +LIKEI_TENCENT_COS_BUCKET=test +LIKEI_TENCENT_COS_REGION=ap-jakarta +LIKEI_TENCENT_COS_ACCESS_URL=https://example.invalid +LIKEI_TENCENT_CLS_SECRET_ID=test +LIKEI_TENCENT_CLS_SECRET_KEY=test +LIKEI_TENCENT_CLS_ENDPOINT=https://cls.tencentcloudapi.com +LIKEI_TENCENT_CLS_REGION=ap-jakarta +LIKEI_TENCENT_CLOUD_IMS_SECRET_ID=test +LIKEI_TENCENT_CLOUD_IMS_SECRET_KEY=test +LIKEI_TENCENT_CLOUD_IMS_ENDPOINT=https://ims.tencentcloudapi.com +LIKEI_TENCENT_CLOUD_IMS_REGION=ap-jakarta +LIKEI_ALIYUN_OSS_ENDPOINT=oss-ap-southeast-1.aliyuncs.com +LIKEI_ALIYUN_OSS_BUCKET_NAME=test +LIKEI_ALIYUN_OSS_ACCESS_URL=https://example.invalid +LIKEI_ALIYUN_OSS_ACCESS_KEY_ID=test +LIKEI_ALIYUN_OSS_ACCESS_KEY_SECRET=test +LIKEI_ALIYUN_STS_ENDPOINT=sts.aliyuncs.com +LIKEI_ALIYUN_STS_ROLE_ARN=test +LIKEI_SMS_ALIYUN_ENABLED=false +LIKEI_SMS_ALIYUN_SIGN_NAME=test +LIKEI_SMS_ALIYUN_ACCESS_KEY_ID=test +LIKEI_SMS_ALIYUN_ACCESS_KEY_SECRET=test +LIKEI_SMS_ALIYUN_ENDPOINT=dysmsapi.aliyuncs.com +LIKEI_CENSOR_ACTIVE=qcloud +LIKEI_CENSOR_TUPU_ENABLED=false +LIKEI_AI_MINIMAX_URL=https://example.invalid +LIKEI_AI_MINIMAX_AUTHORIZATION=test +LIKEI_AI_MINIMAX_MODEL=test +LIKEI_AI_QIANFAN_URL=https://example.invalid +LIKEI_AI_QIANFAN_APP_ID=test +LIKEI_AI_QIANFAN_AUTHORIZATION=test +LIKEI_AI_QIANFAN_MODEL=test +LIKEI_AI_TTS_URL=https://example.invalid +LIKEI_AI_TTS_MODEL=test +LIKEI_TELEGRAM_BOT_TOKEN=test +LIKEI_TELEGRAM_MONITOR_CHAT_ID_1=0 +LIKEI_TELEGRAM_MONITOR_ALERT_LIMIT_SECONDS=300 +LIKEI_TELEGRAM_MONITOR_PROXY_HOST= +LIKEI_TELEGRAM_MONITOR_PROXY_PORT=0 +LIKEI_VISUAL_MONITOR_USERNAME=admin +LIKEI_VISUAL_MONITOR_PASSWORD=admin +LIKEI_GAME_SUD_APP_ID=test +LIKEI_GAME_SUD_APP_KEY=test +LIKEI_GAME_SUD_APP_SECRET=test +LIKEI_GAME_SUD_APP_CHANNEL=test +LIKEI_GAME_BAISHUN_APP_ID=test +LIKEI_GAME_BAISHUN_APP_KEY=test +LIKEI_GAME_BAISHUN_APP_CHANNEL=test +LIKEI_GAME_BAISHUN2FUN_APP_ID=test +LIKEI_GAME_BAISHUN2FUN_APP_KEY=test +LIKEI_GAME_BAISHUN2FUN_APP_CHANNEL=test +LIKEI_GAME_HKYS_SIGN_KEY=test +LIKEI_GAME_HOTGAME_SIGN_KEY=test +LIKEI_YOMI_AES_KEY=test-yomi-aes-key + +# Gateway route targets inside the compose network. +LIKEI_GATEWAY_ROUTE_AUTH_URI=http://auth:1000 +LIKEI_GATEWAY_ROUTE_EXTERNAL_URI=http://external:3000 +LIKEI_GATEWAY_ROUTE_ORDER_URI=http://order:2600 +LIKEI_GATEWAY_ROUTE_WALLET_URI=http://wallet:2000 +LIKEI_GATEWAY_ROUTE_LIVE_URI=http://live:2500 +LIKEI_GATEWAY_ROUTE_OTHER_URI=http://other:2400 +LIKEI_GATEWAY_ROUTE_GO_URI=http://golang:2900 +LIKEI_GATEWAY_ROUTE_INVITE_PUBLIC_URI=http://golang:2900 +LIKEI_GATEWAY_ROUTE_INVITE_APP_URI=http://golang:2900 +LIKEI_GATEWAY_ROUTE_REGISTER_REWARD_APP_URI=http://golang:2900 +LIKEI_GATEWAY_ROUTE_INVITE_INTERNAL_URI=http://golang:2900 +LIKEI_GATEWAY_ROUTE_GAME_APP_URI=http://golang:2900 +LIKEI_GATEWAY_ROUTE_GAME_INTERNAL_URI=http://golang:2900 +LIKEI_GATEWAY_ROUTE_GAME_BAISHUN_URI=http://golang:2900 +LIKEI_GATEWAY_ROUTE_FORWARD_URI=http://other:2400 +LIKEI_GATEWAY_FORWARD_ROUTE_AES_KEY=test-forward-route-aes-key +LIKEI_GATEWAY_FORWARD_AES_KEY=test-forward-aes-key + +# Go API / cron. +CHATAPP_HTTP_LISTEN_ADDR=:2900 +CHATAPP_HTTP_INTERNAL_CALLBACK_SECRET=test-internal-callback-secret +CHATAPP_STORE_MYSQL_DSN=likei:change-me-likei@tcp(mysql:3306)/likei?charset=utf8mb4&parseTime=True&loc=Local +CHATAPP_STORE_REDIS_ADDR=redis:6379 +CHATAPP_STORE_REDIS_PASSWORD= +CHATAPP_STORE_REDIS_DB=0 +CHATAPP_JAVA_AUTH_BASE_URL=http://auth:1000 +CHATAPP_JAVA_CONSOLE_BASE_URL=http://console:2700/console +CHATAPP_JAVA_DEVICE_BASE_URL=http://other:2400 +CHATAPP_JAVA_APP_BASE_URL=http://other:2400 +CHATAPP_JAVA_BRIDGE_BASE_URL=http://console:2700/console +CHATAPP_JAVA_EXTERNAL_BASE_URL=http://external:3000 +CHATAPP_JAVA_OTHER_BASE_URL=http://other:2400 +CHATAPP_JAVA_ORDER_BASE_URL=http://order:2600 +CHATAPP_JAVA_WALLET_BASE_URL=http://wallet:2000 +CHATAPP_INVITE_PUBLIC_BASE_URL=https://jvapi-test.haiyihy.com +CHATAPP_GAME_OPEN_PUBLIC_BASE_URL=https://jvapi-test.haiyihy.com +CHATAPP_WEEK_STAR_TIMEZONE=Asia/Riyadh +CHATAPP_ROOM_TURNOVER_REWARD_TIMEZONE=Asia/Riyadh +CHATAPP_WEEK_STAR_SETTLE_INTERVAL_SECONDS=60 +CHATAPP_ROOM_TURNOVER_REWARD_SETTLE_INTERVAL_SECONDS=60 +CHATAPP_WEEK_STAR_ROCKETMQ_ENABLED=false +CHATAPP_WEEK_STAR_ROCKETMQ_ENDPOINT=rocketmq-proxy:8081 +CHATAPP_WEEK_STAR_ROCKETMQ_ACCESS_KEY=test +CHATAPP_WEEK_STAR_ROCKETMQ_ACCESS_SECRET=test + +# Source locations on the testbox. sync-to-testbox.sh populates these. +TESTBOX_MONITOR_REPO_URL=git@gitea.haiyihy.com:hy/hy-app-monitor.git +TESTBOX_MONITOR_BRANCH=main +JAVA_REPO_URL=git@gitea.haiyihy.com:hy/chatapp3-java.git +JAVA_BRANCH=main +GOLANG_REPO_URL=git@gitea.haiyihy.com:hy/chatapp3-golang.git +GOLANG_BRANCH=main +CRON_REPO_URL=git@gitea.haiyihy.com:hy/chatapp-cron.git +CRON_BRANCH=main +ADMIN_REPO_URL=git@gitea.haiyihy.com:hy/chatapp3-admin.git +ADMIN_BRANCH=main +TESTBOX_SRC_ROOT=/opt/chatapp-test/sources +JAVA_SOURCE_DIR=/opt/chatapp-test/sources/chatapp3-java +GOLANG_SOURCE_DIR=/opt/chatapp-test/sources/chatapp3-golang +CRON_SOURCE_DIR=/opt/chatapp-test/sources/chatapp-cron +ADMIN_SOURCE_DIR=/opt/chatapp-test/sources/chatapp3-admin diff --git a/testbox/.gitignore b/testbox/.gitignore new file mode 100644 index 0000000..9b61749 --- /dev/null +++ b/testbox/.gitignore @@ -0,0 +1,4 @@ +.env +mysql/baseline/*.sql +!mysql/baseline/README.md +!mysql/baseline/.gitkeep diff --git a/testbox/Caddyfile b/testbox/Caddyfile new file mode 100644 index 0000000..508ce73 --- /dev/null +++ b/testbox/Caddyfile @@ -0,0 +1,17 @@ +{ + email admin@haiyihy.com +} + +{$TESTBOX_DOMAIN} { + encode zstd gzip + reverse_proxy gateway:8080 +} + +{$TESTBOX_ADMIN_DOMAIN} { + encode zstd gzip + + @console path /console /console/* + reverse_proxy @console gateway:8080 + + reverse_proxy admin:8080 +} diff --git a/testbox/README.md b/testbox/README.md new file mode 100644 index 0000000..c9418f4 --- /dev/null +++ b/testbox/README.md @@ -0,0 +1,98 @@ +# Chatapp testbox + +Single-node test environment for chatapp Java services, Go API, cron worker, +and local infra. + +Public API entry: + +```text +https://jvapi-test.haiyihy.com +``` + +## What runs here + +- MySQL 8.0 +- Redis 7 +- MongoDB 7 +- Nacos standalone, namespace `RedCircleServiceTest` +- RocketMQ namesrv/broker/proxy +- Java: auth, gateway, external, console, other, live, wallet, order +- Go: chatapp3-golang API +- Cron: chatapp-cron week-star worker +- Admin: chatapp3-admin at `https://yumi-test-admi.haiyihy.com` +- Caddy for HTTPS reverse proxy to gateway + +## First deployment + +Make sure SSH works first: + +```bash +ssh root@43.165.195.39 'hostname' +``` + +Then trigger a git-based deploy: + +```bash +cd /Users/hy/Documents/haiyi/hy-app-monitor +bash testbox/scripts/deploy-from-git.sh root@43.165.195.39 +``` + +The script only SSHes to the testbox. The server pulls these git repositories, +installs Docker on the testbox, starts infra, imports Nacos configs, applies +MySQL baseline files, builds images, and starts apps: + +```text +git@gitea.haiyihy.com:hy/hy-app-monitor.git +git@gitea.haiyihy.com:hy/chatapp3-java.git +git@gitea.haiyihy.com:hy/chatapp3-golang.git +git@gitea.haiyihy.com:hy/chatapp-cron.git +git@gitea.haiyihy.com:hy/chatapp3-admin.git +``` + +After first deployment, you can run the deploy directly on the server: + +```bash +ssh root@43.165.195.39 'bash /opt/chatapp-test/scripts/deploy-from-git-remote.sh' +``` + +The runtime `.env` stays on the server at `/opt/chatapp-test/.env`; git deploys +do not overwrite it. + +## MySQL baseline + +Java services need real schema and base config rows. Put sanitized SQL files in: + +```text +testbox/mysql/baseline/ +``` + +They are applied in lexical order. Recommended: + +```text +10-schema.sql +20-base-data.sql +``` + +The deploy script then applies Go migrations from `chatapp3-golang/migrations` +and Java incremental SQL from `chatapp3-java/sql`. + +## Runtime commands on the server + +```bash +cd /opt/chatapp-test +docker compose --env-file .env -f docker-compose.infra.yml -f docker-compose.apps.yml ps +docker compose --env-file .env -f docker-compose.infra.yml -f docker-compose.apps.yml logs -f gateway +docker compose --env-file .env -f docker-compose.infra.yml -f docker-compose.apps.yml restart other +``` + +Local-only infra ports on the server: + +```text +127.0.0.1:3306 MySQL +127.0.0.1:6379 Redis +127.0.0.1:27017 Mongo +127.0.0.1:8848 Nacos +127.0.0.1:8081 RocketMQ proxy +``` + +Only `80/443` are intended to be public. diff --git a/testbox/config/rocketmq/broker.conf b/testbox/config/rocketmq/broker.conf new file mode 100644 index 0000000..74e8843 --- /dev/null +++ b/testbox/config/rocketmq/broker.conf @@ -0,0 +1,11 @@ +brokerClusterName=DefaultCluster +brokerName=broker-a +brokerId=0 +deleteWhen=04 +fileReservedTime=48 +brokerRole=ASYNC_MASTER +flushDiskType=ASYNC_FLUSH +autoCreateTopicEnable=true +autoCreateSubscriptionGroup=true +listenPort=10911 +namesrvAddr=rocketmq-namesrv:9876 diff --git a/testbox/docker-compose.apps.yml b/testbox/docker-compose.apps.yml new file mode 100644 index 0000000..10149a7 --- /dev/null +++ b/testbox/docker-compose.apps.yml @@ -0,0 +1,302 @@ +x-java-env: &java-env + NACOS_HOST: nacos + NACOS_PORT: "8848" + NACOS_USERNAME: ${NACOS_USERNAME} + NACOS_PASSWORD: ${NACOS_PASSWORD} + SERVER_PROFILE_ACTIVE: ${SERVER_PROFILE_ACTIVE} + LIKEI_MYSQL_JDBC_URL: ${LIKEI_MYSQL_JDBC_URL} + LIKEI_MYSQL_USERNAME: ${LIKEI_MYSQL_USERNAME} + LIKEI_MYSQL_PASSWORD: ${LIKEI_MYSQL_PASSWORD} + LIKEI_REDIS_HOST: ${LIKEI_REDIS_HOST} + LIKEI_REDIS_PORT: ${LIKEI_REDIS_PORT} + LIKEI_REDIS_DATABASE: ${LIKEI_REDIS_DATABASE} + LIKEI_MONGO_URI: ${LIKEI_MONGO_URI} + LIKEI_ROCKETMQ_ACCESS_KEY: ${LIKEI_ROCKETMQ_ACCESS_KEY} + LIKEI_ROCKETMQ_SECRET_KEY: ${LIKEI_ROCKETMQ_SECRET_KEY} + LIKEI_ROCKETMQ_ENDPOINT: ${LIKEI_ROCKETMQ_ENDPOINT} + LIKEI_ROCKETMQ_LOCAL_DISABLE: ${LIKEI_ROCKETMQ_LOCAL_DISABLE} + LIKEI_GATEWAY_ROUTE_AUTH_URI: ${LIKEI_GATEWAY_ROUTE_AUTH_URI} + LIKEI_GATEWAY_ROUTE_EXTERNAL_URI: ${LIKEI_GATEWAY_ROUTE_EXTERNAL_URI} + LIKEI_GATEWAY_ROUTE_ORDER_URI: ${LIKEI_GATEWAY_ROUTE_ORDER_URI} + LIKEI_GATEWAY_ROUTE_WALLET_URI: ${LIKEI_GATEWAY_ROUTE_WALLET_URI} + LIKEI_GATEWAY_ROUTE_LIVE_URI: ${LIKEI_GATEWAY_ROUTE_LIVE_URI} + LIKEI_GATEWAY_ROUTE_OTHER_URI: ${LIKEI_GATEWAY_ROUTE_OTHER_URI} + LIKEI_GATEWAY_ROUTE_GO_URI: ${LIKEI_GATEWAY_ROUTE_GO_URI} + LIKEI_GATEWAY_ROUTE_INVITE_PUBLIC_URI: ${LIKEI_GATEWAY_ROUTE_INVITE_PUBLIC_URI} + LIKEI_GATEWAY_ROUTE_INVITE_APP_URI: ${LIKEI_GATEWAY_ROUTE_INVITE_APP_URI} + LIKEI_GATEWAY_ROUTE_REGISTER_REWARD_APP_URI: ${LIKEI_GATEWAY_ROUTE_REGISTER_REWARD_APP_URI} + LIKEI_GATEWAY_ROUTE_INVITE_INTERNAL_URI: ${LIKEI_GATEWAY_ROUTE_INVITE_INTERNAL_URI} + LIKEI_GATEWAY_ROUTE_GAME_APP_URI: ${LIKEI_GATEWAY_ROUTE_GAME_APP_URI} + LIKEI_GATEWAY_ROUTE_GAME_INTERNAL_URI: ${LIKEI_GATEWAY_ROUTE_GAME_INTERNAL_URI} + LIKEI_GATEWAY_ROUTE_GAME_BAISHUN_URI: ${LIKEI_GATEWAY_ROUTE_GAME_BAISHUN_URI} + LIKEI_GATEWAY_ROUTE_FORWARD_URI: ${LIKEI_GATEWAY_ROUTE_FORWARD_URI} + LIKEI_GATEWAY_FORWARD_ROUTE_AES_KEY: ${LIKEI_GATEWAY_FORWARD_ROUTE_AES_KEY} + LIKEI_GATEWAY_FORWARD_AES_KEY: ${LIKEI_GATEWAY_FORWARD_AES_KEY} + +x-java-small: &java-small + JVM_OPTION: "-Xms${JAVA_XMS} -Xmx${JAVA_XMX_SMALL} -XX:MaxMetaspaceSize=${JAVA_METASPACE} -Djava.security.egd=file:/dev/./urandom" + +x-java-medium: &java-medium + JVM_OPTION: "-Xms${JAVA_XMS} -Xmx${JAVA_XMX_MEDIUM} -XX:MaxMetaspaceSize=${JAVA_METASPACE} -Djava.security.egd=file:/dev/./urandom" + +x-app-depends: &app-depends + mysql: + condition: service_healthy + redis: + condition: service_healthy + mongo: + condition: service_healthy + nacos: + condition: service_healthy + +services: + auth: + build: + context: ${JAVA_SOURCE_DIR}/rc-auth + args: + SERVICE_VERSION: testbox + image: chatapp-test/auth:testbox + container_name: chatapp-test-auth + restart: unless-stopped + depends_on: *app-depends + env_file: + - .env + environment: + <<: [*java-env, *java-small] + SERVER_PORT: "1000" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:1000/actuator/health | grep -q UP"] + interval: 15s + timeout: 5s + retries: 40 + + wallet: + build: + context: ${JAVA_SOURCE_DIR}/rc-service/rc-service-wallet + args: + SERVICE_VERSION: testbox + image: chatapp-test/wallet:testbox + container_name: chatapp-test-wallet + restart: unless-stopped + depends_on: *app-depends + env_file: + - .env + environment: + <<: [*java-env, *java-medium] + SERVER_PORT: "2000" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:2000/actuator/health | grep -q UP"] + interval: 15s + timeout: 5s + retries: 40 + + other: + build: + context: ${JAVA_SOURCE_DIR}/rc-service/rc-service-other + args: + SERVICE_VERSION: testbox + image: chatapp-test/other:testbox + container_name: chatapp-test-other + restart: unless-stopped + depends_on: *app-depends + env_file: + - .env + environment: + <<: [*java-env, *java-medium] + SERVER_PORT: "2400" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:2400/actuator/health | grep -q UP"] + interval: 15s + timeout: 5s + retries: 40 + + live: + build: + context: ${JAVA_SOURCE_DIR}/rc-service/rc-service-live + args: + SERVICE_VERSION: testbox + image: chatapp-test/live:testbox + container_name: chatapp-test-live + restart: unless-stopped + depends_on: *app-depends + env_file: + - .env + environment: + <<: [*java-env, *java-small] + SERVER_PORT: "2500" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:2500/actuator/health | grep -q UP"] + interval: 15s + timeout: 5s + retries: 40 + + order: + build: + context: ${JAVA_SOURCE_DIR}/rc-service/rc-service-order + args: + SERVICE_VERSION: testbox + image: chatapp-test/order:testbox + container_name: chatapp-test-order + restart: unless-stopped + depends_on: *app-depends + env_file: + - .env + environment: + <<: [*java-env, *java-medium] + SERVER_PORT: "2600" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:2600/actuator/health | grep -q UP"] + interval: 15s + timeout: 5s + retries: 40 + + console: + build: + context: ${JAVA_SOURCE_DIR}/rc-service/rc-service-console + args: + SERVICE_VERSION: testbox + image: chatapp-test/console:testbox + container_name: chatapp-test-console + restart: unless-stopped + depends_on: *app-depends + env_file: + - .env + environment: + <<: [*java-env, *java-medium] + SERVER_PORT: "2700" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:2700/console/actuator/health | grep -q UP"] + interval: 15s + timeout: 5s + retries: 40 + + external: + build: + context: ${JAVA_SOURCE_DIR}/rc-service/rc-service-external + args: + SERVICE_VERSION: testbox + image: chatapp-test/external:testbox + container_name: chatapp-test-external + restart: unless-stopped + depends_on: *app-depends + env_file: + - .env + environment: + <<: [*java-env, *java-small] + SERVER_PORT: "3000" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/actuator/health | grep -q UP"] + interval: 15s + timeout: 5s + retries: 40 + + golang: + build: + context: ${GOLANG_SOURCE_DIR} + args: + APP_VERSION: testbox + image: chatapp-test/golang:testbox + container_name: chatapp-test-golang + restart: unless-stopped + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + env_file: + - .env + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:2900/health | grep -q ok"] + interval: 15s + timeout: 5s + retries: 30 + + chatapp-cron: + build: + context: ${CRON_SOURCE_DIR} + args: + APP_VERSION: testbox + image: chatapp-test/chatapp-cron:testbox + container_name: chatapp-test-cron + restart: unless-stopped + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + other: + condition: service_started + wallet: + condition: service_started + env_file: + - .env + + admin: + image: chatapp-test/admin:testbox + container_name: chatapp-test-admin + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/ | grep -qi ' + sh -lc 'if [ -n "$$REDIS_PASSWORD" ]; then + exec redis-server --appendonly yes --requirepass "$$REDIS_PASSWORD"; + else exec redis-server --appendonly yes; fi' + environment: + REDIS_PASSWORD: ${REDIS_PASSWORD} + TZ: Asia/Shanghai + ports: + - "127.0.0.1:6379:6379" + volumes: + - redis-data:/data + healthcheck: + test: ["CMD-SHELL", "redis-cli ping | grep -q PONG"] + interval: 10s + timeout: 5s + retries: 30 + + mongo: + image: mongo:7.0 + container_name: chatapp-test-mongo + restart: unless-stopped + environment: + TZ: Asia/Shanghai + ports: + - "127.0.0.1:27017:27017" + volumes: + - mongo-data:/data/db + healthcheck: + test: ["CMD-SHELL", "mongosh --quiet --eval 'db.runCommand({ ping: 1 }).ok' | grep -q 1"] + interval: 10s + timeout: 5s + retries: 30 + + nacos: + image: nacos/nacos-server:v2.3.2 + container_name: chatapp-test-nacos + restart: unless-stopped + environment: + MODE: standalone + PREFER_HOST_MODE: hostname + NACOS_AUTH_ENABLE: "false" + JVM_XMS: 512m + JVM_XMX: 512m + JVM_XMN: 256m + TZ: Asia/Shanghai + ports: + - "127.0.0.1:8848:8848" + - "127.0.0.1:9848:9848" + volumes: + - nacos-data:/home/nacos/data + - nacos-logs:/home/nacos/logs + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8848/nacos/v1/console/health/readiness | grep -Eq '^(UP|OK)$'"] + interval: 10s + timeout: 5s + retries: 60 + + rocketmq-namesrv: + image: apache/rocketmq:5.3.1 + container_name: chatapp-test-rocketmq-namesrv + user: root + restart: unless-stopped + command: sh mqnamesrv + environment: + JAVA_OPT_EXT: "-Xms256m -Xmx256m -Xmn128m" + TZ: Asia/Shanghai + ports: + - "127.0.0.1:9876:9876" + volumes: + - rocketmq-namesrv-logs:/home/rocketmq/logs + healthcheck: + test: ["CMD-SHELL", "bash -lc ' /etc/nginx/conf.d/mjs.conf \ + && rm -rf /etc/nginx/conf.d/default.conf + +COPY --from=builder /app/apps/dist /usr/share/nginx/html +COPY --from=builder /app/scripts/deploy/nginx.conf /etc/nginx/nginx.conf + +EXPOSE 8080 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/testbox/mysql/baseline/.gitkeep b/testbox/mysql/baseline/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/testbox/mysql/baseline/README.md b/testbox/mysql/baseline/README.md new file mode 100644 index 0000000..3514232 --- /dev/null +++ b/testbox/mysql/baseline/README.md @@ -0,0 +1,8 @@ +Put repeatable test baseline SQL files here. + +Suggested files: + +- 10-schema.sql: schema-only dump from production. +- 20-base-data.sql: whitelisted config/base data only. + +The deploy script applies files in lexical order before Go migrations. diff --git a/testbox/mysql/init/00-create-db.sql b/testbox/mysql/init/00-create-db.sql new file mode 100644 index 0000000..f0d178d --- /dev/null +++ b/testbox/mysql/init/00-create-db.sql @@ -0,0 +1 @@ +CREATE DATABASE IF NOT EXISTS `likei` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/testbox/scripts/apply-mysql-baseline.sh b/testbox/scripts/apply-mysql-baseline.sh new file mode 100755 index 0000000..77e1599 --- /dev/null +++ b/testbox/scripts/apply-mysql-baseline.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +env_value() { + python3 "$ROOT_DIR/scripts/env-value.py" "$ROOT_DIR/.env" "$1" "${2:-}" +} + +MYSQL_CONTAINER="${MYSQL_CONTAINER:-$(env_value MYSQL_CONTAINER chatapp-test-mysql)}" +MYSQL_DATABASE="${MYSQL_DATABASE:-$(env_value MYSQL_DATABASE likei)}" +MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-$(env_value MYSQL_ROOT_PASSWORD)}" +GOLANG_SOURCE_DIR="${GOLANG_SOURCE_DIR:-$(env_value GOLANG_SOURCE_DIR "$ROOT_DIR/sources/chatapp3-golang")}" +JAVA_SOURCE_DIR="${JAVA_SOURCE_DIR:-$(env_value JAVA_SOURCE_DIR "$ROOT_DIR/sources/chatapp3-java")}" +TESTBOX_APPLY_JAVA_SQL="${TESTBOX_APPLY_JAVA_SQL:-$(env_value TESTBOX_APPLY_JAVA_SQL 1)}" + +if [ -z "$MYSQL_ROOT_PASSWORD" ]; then + echo "MYSQL_ROOT_PASSWORD is required" >&2 + exit 1 +fi + +mysql_exec() { + docker exec -i "$MYSQL_CONTAINER" mysql -uroot -p"$MYSQL_ROOT_PASSWORD" "$@" +} + +echo "waiting mysql..." +for _ in $(seq 1 90); do + if docker exec "$MYSQL_CONTAINER" mysqladmin ping -h 127.0.0.1 -uroot -p"$MYSQL_ROOT_PASSWORD" --silent >/dev/null 2>&1; then + break + fi + sleep 2 +done + +mysql_exec -e "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" + +shopt -s nullglob +baseline_files=("$ROOT_DIR"/mysql/baseline/*.sql) +if [ "${#baseline_files[@]}" -eq 0 ]; then + echo "warn: no mysql baseline SQL found in mysql/baseline; Java services need schema/base data before full use" >&2 +else + for sql in "${baseline_files[@]}"; do + echo "applying baseline $(basename "$sql")" + mysql_exec "$MYSQL_DATABASE" < "$sql" + done +fi + +GO_MIGRATIONS_DIR="$GOLANG_SOURCE_DIR/migrations" +if [ -d "$GO_MIGRATIONS_DIR" ]; then + for sql in "$GO_MIGRATIONS_DIR"/*.sql; do + [ -f "$sql" ] || continue + echo "applying go migration $(basename "$sql")" + mysql_exec "$MYSQL_DATABASE" < "$sql" || { + echo "warn: go migration failed, continuing: $sql" >&2 + } + done +fi + +JAVA_SQL_DIR="$JAVA_SOURCE_DIR/sql" +if [ "${TESTBOX_APPLY_JAVA_SQL:-1}" = "1" ] && [ -d "$JAVA_SQL_DIR" ]; then + for sql in "$JAVA_SQL_DIR"/*.sql; do + [ -f "$sql" ] || continue + echo "applying java sql $(basename "$sql")" + mysql_exec "$MYSQL_DATABASE" < "$sql" || { + echo "warn: java sql failed, continuing: $sql" >&2 + } + done +fi + +echo "mysql baseline step done" diff --git a/testbox/scripts/bootstrap-server.sh b/testbox/scripts/bootstrap-server.sh new file mode 100755 index 0000000..90796c4 --- /dev/null +++ b/testbox/scripts/bootstrap-server.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +if [ "$(id -u)" -ne 0 ]; then + echo "bootstrap-server.sh must run as root" >&2 + exit 1 +fi + +install_packages() { + if command -v dnf >/dev/null 2>&1; then + dnf -y install curl rsync git tar gzip python3 python3-pip dnf-plugins-core ca-certificates java-17-openjdk-devel maven >/dev/null + if ! command -v docker >/dev/null 2>&1; then + dnf -y install docker docker-compose-plugin >/dev/null 2>&1 || { + dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo >/dev/null + dnf -y install docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null + } + fi + elif command -v apt-get >/dev/null 2>&1; then + apt-get update >/dev/null + apt-get install -y curl rsync git tar gzip python3 python3-pip ca-certificates openjdk-17-jdk maven docker.io docker-compose-plugin >/dev/null + else + echo "unsupported Linux distribution: missing dnf/apt-get" >&2 + exit 1 + fi +} + +install_packages + +systemctl enable --now docker >/dev/null + +mkdir -p "$ROOT_DIR"/{sources,mysql/baseline,run,logs} +sysctl -w vm.max_map_count=262144 >/dev/null || true + +if command -v firewall-cmd >/dev/null 2>&1 && systemctl is-active --quiet firewalld; then + firewall-cmd --add-service=http --permanent >/dev/null || true + firewall-cmd --add-service=https --permanent >/dev/null || true + firewall-cmd --reload >/dev/null || true +fi + +if [ ! -f "$ROOT_DIR/.env" ]; then + cp "$ROOT_DIR/.env.example" "$ROOT_DIR/.env" + echo "created $ROOT_DIR/.env from example; please review passwords before real use" >&2 +fi + +docker compose version diff --git a/testbox/scripts/build-images.sh b/testbox/scripts/build-images.sh new file mode 100755 index 0000000..f77d137 --- /dev/null +++ b/testbox/scripts/build-images.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +env_value() { + python3 "$ROOT_DIR/scripts/env-value.py" "$ROOT_DIR/.env" "$1" "${2:-}" +} + +JAVA_SOURCE_DIR="${JAVA_SOURCE_DIR:-$(env_value JAVA_SOURCE_DIR "$ROOT_DIR/sources/chatapp3-java")}" +ADMIN_SOURCE_DIR="${ADMIN_SOURCE_DIR:-$(env_value ADMIN_SOURCE_DIR "$ROOT_DIR/sources/chatapp3-admin")}" +TESTBOX_ADMIN_API_BASE_URL="${TESTBOX_ADMIN_API_BASE_URL:-$(env_value TESTBOX_ADMIN_API_BASE_URL "https://yumi-test-admi.haiyihy.com/console")}" +TESTBOX_ADMIN_OSS_URL="${TESTBOX_ADMIN_OSS_URL:-$(env_value TESTBOX_ADMIN_OSS_URL "")}" + +if [ ! -d "$JAVA_SOURCE_DIR" ]; then + echo "missing JAVA_SOURCE_DIR: $JAVA_SOURCE_DIR" >&2 + exit 1 +fi + +if [ ! -d "$ADMIN_SOURCE_DIR" ]; then + echo "missing ADMIN_SOURCE_DIR: $ADMIN_SOURCE_DIR" >&2 + exit 1 +fi + +cd "$JAVA_SOURCE_DIR" +if [ -x ./ci/install-private-maven.sh ]; then + ./ci/install-private-maven.sh +fi + +JAVA_MODULES="rc-auth,rc-gateway,rc-service/rc-service-external/external-start,rc-service/rc-service-console/console-start,rc-service/rc-service-other/other-start,rc-service/rc-service-live/live-start,rc-service/rc-service-wallet/wallet-start,rc-service/rc-service-order/order-start" +mvn -Ptest -T 1C -pl "$JAVA_MODULES" -am -Dmaven.test.skip=true package + +cd "$ROOT_DIR" +docker compose --env-file .env -f docker-compose.infra.yml -f docker-compose.apps.yml build \ + auth gateway external console other live wallet order golang chatapp-cron + +docker build \ + -f "$ROOT_DIR/docker/chatapp3-admin.Dockerfile" \ + --build-arg "ADMIN_API_BASE_URL=$TESTBOX_ADMIN_API_BASE_URL" \ + --build-arg "ADMIN_OSS_URL=$TESTBOX_ADMIN_OSS_URL" \ + -t chatapp-test/admin:testbox \ + "$ADMIN_SOURCE_DIR" diff --git a/testbox/scripts/create-rocketmq-topics.sh b/testbox/scripts/create-rocketmq-topics.sh new file mode 100755 index 0000000..6271e27 --- /dev/null +++ b/testbox/scripts/create-rocketmq-topics.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +NAMESRV="${ROCKETMQ_NAMESRV_ADDR:-rocketmq-namesrv:9876}" +CLUSTER="${ROCKETMQ_CLUSTER:-DefaultCluster}" +BROKER_CONTAINER="${ROCKETMQ_BROKER_CONTAINER:-chatapp-test-rocketmq-broker}" +MQADMIN="/home/rocketmq/rocketmq-5.3.1/bin/mqadmin" + +topics=( + RC_DEFAULT_APP_ORDINARY + RC_DEFAULT_APP_DELAYED + RC_DEFAULT_APP_ORDER +) + +for topic in "${topics[@]}"; do + docker exec "$BROKER_CONTAINER" sh -lc \ + "$MQADMIN updateTopic -n '$NAMESRV' -c '$CLUSTER' -t '$topic' >/dev/null" + echo "rocketmq topic ok: $topic" +done diff --git a/testbox/scripts/deploy-all.sh b/testbox/scripts/deploy-all.sh new file mode 100755 index 0000000..2de719c --- /dev/null +++ b/testbox/scripts/deploy-all.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +if [ ! -f .env ]; then + cp .env.example .env + echo "created .env from .env.example; edit credentials and rerun" >&2 + exit 2 +fi + +scripts/merge-env-defaults.sh .env .env.example + +COMPOSE=(docker compose --env-file .env -f docker-compose.infra.yml -f docker-compose.apps.yml) + +"${COMPOSE[@]}" up -d mysql redis mongo nacos rocketmq-namesrv rocketmq-broker rocketmq-proxy + +python3 scripts/import-nacos-configs.py +scripts/create-rocketmq-topics.sh +scripts/apply-mysql-baseline.sh +scripts/build-images.sh + +"${COMPOSE[@]}" up -d auth wallet other live order console external golang gateway chatapp-cron admin caddy + +echo +echo "testbox status:" +"${COMPOSE[@]}" ps +echo +echo "public url: ${TESTBOX_PUBLIC_BASE_URL:-https://jvapi-test.haiyihy.com}" diff --git a/testbox/scripts/deploy-from-git-remote.sh b/testbox/scripts/deploy-from-git-remote.sh new file mode 100755 index 0000000..5af2109 --- /dev/null +++ b/testbox/scripts/deploy-from-git-remote.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +set -euo pipefail + +REMOTE_DIR="${REMOTE_DIR:-/opt/chatapp-test}" +MONITOR_CHECKOUT_DIR="${MONITOR_CHECKOUT_DIR:-$REMOTE_DIR/git/hy-app-monitor}" +export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o StrictHostKeyChecking=accept-new}" + +env_file_value() { + local key="$1" + local file="${2:-$REMOTE_DIR/.env}" + [ -f "$file" ] || return 0 + awk -v key="$key" ' + BEGIN { FS = "=" } + $1 == key { + sub(/^[^=]*=/, "") + gsub(/^[ \t"'"'"']+|[ \t"'"'"']+$/, "") + print + exit + } + ' "$file" +} + +MONITOR_REPO_URL="${MONITOR_REPO_URL:-$(env_file_value TESTBOX_MONITOR_REPO_URL)}" +MONITOR_REPO_URL="${MONITOR_REPO_URL:-git@gitea.haiyihy.com:hy/hy-app-monitor.git}" +MONITOR_BRANCH="${MONITOR_BRANCH:-$(env_file_value TESTBOX_MONITOR_BRANCH)}" +MONITOR_BRANCH="${MONITOR_BRANCH:-main}" + +sync_repo() { + local dir="$1" + local url="$2" + local branch="$3" + local stamp + + mkdir -p "$(dirname "$dir")" + + if [ -e "$dir" ] && [ ! -d "$dir/.git" ]; then + stamp="$(date +%Y%m%d%H%M%S)" + mv "$dir" "$dir.pre-git-$stamp" + echo "moved non-git directory to $dir.pre-git-$stamp" + fi + + if [ ! -d "$dir/.git" ]; then + git clone --branch "$branch" "$url" "$dir" + fi + + git -C "$dir" remote set-url origin "$url" + git -C "$dir" fetch --prune origin "$branch" + git -C "$dir" checkout -B "$branch" "origin/$branch" + git -C "$dir" reset --hard "origin/$branch" +} + +sync_repo "$MONITOR_CHECKOUT_DIR" "$MONITOR_REPO_URL" "$MONITOR_BRANCH" + +latest_script="$MONITOR_CHECKOUT_DIR/testbox/scripts/deploy-from-git-remote.sh" +current_script="${BASH_SOURCE[0]}" +if [ "${TESTBOX_REMOTE_DEPLOY_BOOTSTRAPPED:-0}" != "1" ] && [ "$current_script" != "$latest_script" ]; then + export TESTBOX_REMOTE_DEPLOY_BOOTSTRAPPED=1 + exec bash "$latest_script" +fi + +mkdir -p "$REMOTE_DIR"/{git,sources,run,logs} + +rsync --archive --delete \ + --exclude ".env" \ + --exclude "git" \ + --exclude "sources" \ + --exclude "run" \ + --exclude "logs" \ + --exclude "mysql/baseline/*.sql" \ + "$MONITOR_CHECKOUT_DIR/testbox/" "$REMOTE_DIR/" + +bash "$REMOTE_DIR/scripts/bootstrap-server.sh" +bash "$REMOTE_DIR/scripts/merge-env-defaults.sh" "$REMOTE_DIR/.env" "$REMOTE_DIR/.env.example" + +env_value() { + python3 "$REMOTE_DIR/scripts/env-value.py" "$REMOTE_DIR/.env" "$1" "${2:-}" +} + +JAVA_REPO_URL="$(env_value JAVA_REPO_URL git@gitea.haiyihy.com:hy/chatapp3-java.git)" +JAVA_BRANCH="$(env_value JAVA_BRANCH main)" +GOLANG_REPO_URL="$(env_value GOLANG_REPO_URL git@gitea.haiyihy.com:hy/chatapp3-golang.git)" +GOLANG_BRANCH="$(env_value GOLANG_BRANCH main)" +CRON_REPO_URL="$(env_value CRON_REPO_URL git@gitea.haiyihy.com:hy/chatapp-cron.git)" +CRON_BRANCH="$(env_value CRON_BRANCH main)" +ADMIN_REPO_URL="$(env_value ADMIN_REPO_URL git@gitea.haiyihy.com:hy/chatapp3-admin.git)" +ADMIN_BRANCH="$(env_value ADMIN_BRANCH main)" + +JAVA_SOURCE_DIR="$(env_value JAVA_SOURCE_DIR "$REMOTE_DIR/sources/chatapp3-java")" +GOLANG_SOURCE_DIR="$(env_value GOLANG_SOURCE_DIR "$REMOTE_DIR/sources/chatapp3-golang")" +CRON_SOURCE_DIR="$(env_value CRON_SOURCE_DIR "$REMOTE_DIR/sources/chatapp-cron")" +ADMIN_SOURCE_DIR="$(env_value ADMIN_SOURCE_DIR "$REMOTE_DIR/sources/chatapp3-admin")" + +echo "updating source repos from git..." +sync_repo "$JAVA_SOURCE_DIR" "$JAVA_REPO_URL" "$JAVA_BRANCH" +sync_repo "$GOLANG_SOURCE_DIR" "$GOLANG_REPO_URL" "$GOLANG_BRANCH" +sync_repo "$CRON_SOURCE_DIR" "$CRON_REPO_URL" "$CRON_BRANCH" +sync_repo "$ADMIN_SOURCE_DIR" "$ADMIN_REPO_URL" "$ADMIN_BRANCH" + +cd "$REMOTE_DIR" +bash scripts/deploy-all.sh diff --git a/testbox/scripts/deploy-from-git.sh b/testbox/scripts/deploy-from-git.sh new file mode 100755 index 0000000..c94ffe8 --- /dev/null +++ b/testbox/scripts/deploy-from-git.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +REMOTE="${1:-root@43.165.195.39}" +REMOTE_DIR="${REMOTE_DIR:-/opt/chatapp-test}" +MONITOR_REPO_URL="${MONITOR_REPO_URL:-git@gitea.haiyihy.com:hy/hy-app-monitor.git}" +MONITOR_BRANCH="${MONITOR_BRANCH:-main}" +MONITOR_CHECKOUT_DIR="${MONITOR_CHECKOUT_DIR:-$REMOTE_DIR/git/hy-app-monitor}" + +ssh "$REMOTE" \ + "REMOTE_DIR='$REMOTE_DIR' MONITOR_REPO_URL='$MONITOR_REPO_URL' MONITOR_BRANCH='$MONITOR_BRANCH' MONITOR_CHECKOUT_DIR='$MONITOR_CHECKOUT_DIR' bash -s" <<'REMOTE_BOOTSTRAP' +set -euo pipefail + +REMOTE_DIR="${REMOTE_DIR:-/opt/chatapp-test}" +MONITOR_REPO_URL="${MONITOR_REPO_URL:-git@gitea.haiyihy.com:hy/hy-app-monitor.git}" +MONITOR_BRANCH="${MONITOR_BRANCH:-main}" +MONITOR_CHECKOUT_DIR="${MONITOR_CHECKOUT_DIR:-$REMOTE_DIR/git/hy-app-monitor}" +export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o StrictHostKeyChecking=accept-new}" + +install_minimal_packages() { + if command -v git >/dev/null 2>&1 && command -v rsync >/dev/null 2>&1; then + return + fi + + if command -v dnf >/dev/null 2>&1; then + dnf -y install git rsync ca-certificates >/dev/null + elif command -v apt-get >/dev/null 2>&1; then + apt-get update >/dev/null + apt-get install -y git rsync ca-certificates >/dev/null + else + echo "unsupported Linux distribution: missing dnf/apt-get" >&2 + exit 1 + fi +} + +sync_repo() { + local dir="$1" + local url="$2" + local branch="$3" + local stamp + + mkdir -p "$(dirname "$dir")" + + if [ -e "$dir" ] && [ ! -d "$dir/.git" ]; then + stamp="$(date +%Y%m%d%H%M%S)" + mv "$dir" "$dir.pre-git-$stamp" + echo "moved non-git directory to $dir.pre-git-$stamp" + fi + + if [ ! -d "$dir/.git" ]; then + git clone --branch "$branch" "$url" "$dir" + fi + + git -C "$dir" remote set-url origin "$url" + git -C "$dir" fetch --prune origin "$branch" + git -C "$dir" checkout -B "$branch" "origin/$branch" + git -C "$dir" reset --hard "origin/$branch" +} + +install_minimal_packages +sync_repo "$MONITOR_CHECKOUT_DIR" "$MONITOR_REPO_URL" "$MONITOR_BRANCH" + +exec bash "$MONITOR_CHECKOUT_DIR/testbox/scripts/deploy-from-git-remote.sh" +REMOTE_BOOTSTRAP diff --git a/testbox/scripts/env-value.py b/testbox/scripts/env-value.py new file mode 100755 index 0000000..d081200 --- /dev/null +++ b/testbox/scripts/env-value.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import sys +from pathlib import Path + + +def read_value(path: Path, key: str, default: str) -> str: + if not path.exists(): + return default + for raw in path.read_text(encoding="utf-8").splitlines(): + line = raw.strip() + if not line or line.startswith("#") or "=" not in line: + continue + name, value = line.split("=", 1) + if name.strip() == key: + return value.strip().strip('"').strip("'") + return default + + +def main() -> int: + if len(sys.argv) < 3: + print("usage: env-value.py ENV_FILE KEY [DEFAULT]", file=sys.stderr) + return 2 + default = sys.argv[3] if len(sys.argv) > 3 else "" + print(read_value(Path(sys.argv[1]), sys.argv[2], default)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/testbox/scripts/import-nacos-configs.py b/testbox/scripts/import-nacos-configs.py new file mode 100755 index 0000000..4674144 --- /dev/null +++ b/testbox/scripts/import-nacos-configs.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import os +from pathlib import Path +import sys +import time +import urllib.parse +import urllib.request + + +ROOT = Path(__file__).resolve().parents[1] +ENV_PATH = ROOT / ".env" + + +def load_env(path: Path) -> dict[str, str]: + values: dict[str, str] = {} + if not path.exists(): + return values + for raw in path.read_text(encoding="utf-8").splitlines(): + line = raw.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + values[key.strip()] = value.strip().strip('"').strip("'") + return values + + +ENV = {**load_env(ENV_PATH), **os.environ} +NACOS_BASE_URL = ENV.get("NACOS_BASE_URL", "http://127.0.0.1:8848").rstrip("/") +NAMESPACE_ID = ENV.get("NACOS_NAMESPACE_ID", "RedCircleServiceTest") +NAMESPACE_NAME = ENV.get("NACOS_NAMESPACE_NAME", NAMESPACE_ID) +JAVA_SOURCE_DIR = Path(ENV.get("JAVA_SOURCE_DIR", str(ROOT / "sources" / "chatapp3-java"))) + + +def request(method: str, path: str, data: dict[str, str] | None = None) -> str: + url = f"{NACOS_BASE_URL}{path}" + body = None + headers = {"User-Agent": "chatapp-testbox/1.0"} + if data is not None: + body = urllib.parse.urlencode(data).encode("utf-8") + headers["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8" + req = urllib.request.Request(url, data=body, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=15) as resp: + return resp.read().decode("utf-8", errors="replace") + + +def wait_nacos() -> None: + for _ in range(90): + try: + text = request("GET", "/nacos/v1/console/health/readiness") + if "UP" in text or "OK" in text: + return + except Exception: + pass + time.sleep(2) + raise RuntimeError("nacos readiness timeout") + + +def ensure_namespace() -> None: + data = { + "customNamespaceId": NAMESPACE_ID, + "namespaceName": NAMESPACE_NAME, + "namespaceDesc": "single-node chatapp testbox", + } + try: + request("POST", "/nacos/v1/console/namespaces", data) + except Exception as exc: + print(f"warn: namespace create skipped: {exc}", file=sys.stderr) + + +def publish_config(group: str, data_id: str, content: str) -> None: + payload = { + "tenant": NAMESPACE_ID, + "group": group, + "dataId": data_id, + "type": "yaml" if data_id.endswith((".yml", ".yaml")) else "text", + "content": content, + } + result = request("POST", "/nacos/v1/cs/configs", payload).strip() + if result.lower() == "false": + raise RuntimeError(f"nacos rejected config {group}/{data_id}") + + +def main() -> int: + config_root = JAVA_SOURCE_DIR / "nacos_config" + if not config_root.is_dir(): + raise RuntimeError(f"missing nacos_config: {config_root}") + + wait_nacos() + ensure_namespace() + + count = 0 + for path in sorted(config_root.glob("*/*")): + if not path.is_file(): + continue + group = path.parent.name + data_id = path.name + publish_config(group, data_id, path.read_text(encoding="utf-8")) + count += 1 + print(f"published {group}/{data_id}") + + print(f"nacos import ok: namespace={NAMESPACE_ID} configs={count}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/testbox/scripts/merge-env-defaults.sh b/testbox/scripts/merge-env-defaults.sh new file mode 100755 index 0000000..1bb63c0 --- /dev/null +++ b/testbox/scripts/merge-env-defaults.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +env_file="${1:-.env}" +defaults_file="${2:-.env.example}" + +if [ ! -f "$env_file" ] || [ ! -f "$defaults_file" ]; then + exit 0 +fi + +added=0 +while IFS= read -r line || [ -n "$line" ]; do + trimmed="${line#"${line%%[![:space:]]*}"}" + if [ -z "$trimmed" ] || [[ "$trimmed" == \#* ]] || [[ "$trimmed" != *=* ]]; then + continue + fi + key="${trimmed%%=*}" + if ! grep -qE "^${key}=" "$env_file"; then + if [ "$added" -eq 0 ]; then + { + echo + echo "# Added from .env.example on $(date +%F)" + } >> "$env_file" + fi + echo "$trimmed" >> "$env_file" + added=$((added + 1)) + fi +done < "$defaults_file" + +if [ "$added" -gt 0 ]; then + echo "merged $added missing env defaults into $env_file" +fi diff --git a/testbox/scripts/sync-to-testbox.sh b/testbox/scripts/sync-to-testbox.sh new file mode 100755 index 0000000..b0a0de0 --- /dev/null +++ b/testbox/scripts/sync-to-testbox.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +REMOTE="${1:-root@43.165.195.39}" + +echo "sync-to-testbox.sh is now a git-based deploy wrapper." +echo "No local source files or local .env will be copied." +exec "$ROOT_DIR/scripts/deploy-from-git.sh" "$REMOTE"