358 lines
16 KiB
Makefile
358 lines
16 KiB
Makefile
# go-zero 生成風格
|
||
GO_ZERO_STYLE := go_zero
|
||
GO ?= go
|
||
GOFMT ?= gofmt
|
||
GOFILES := $(shell find . -name '*.go' -not -path './generate/doc-generate/*')
|
||
|
||
GO_DOC_DIR := generate/doc-generate
|
||
GO_DOC_BIN := $(GO_DOC_DIR)/bin/go-doc
|
||
API_ENTRY := ./generate/api/gateway.api
|
||
DOC_OUT := ./docs/openapi
|
||
|
||
GOCTL ?= goctl
|
||
GOCTL_PKG := github.com/zeromicro/go-zero/tools/goctl@latest
|
||
GOLANGCI_PKG := github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
|
||
|
||
.DEFAULT_GOAL := help
|
||
|
||
help: ## 顯示可用指令
|
||
@echo "Gateway Makefile"
|
||
@echo ""
|
||
@echo "首次開發:"
|
||
@echo " make tools 安裝 goctl、goimports、golangci-lint(寫入 \$$GOPATH/bin)"
|
||
@echo " go mod download"
|
||
@echo ""
|
||
@echo "常用:"
|
||
@grep -E '^[a-zA-Z0-9_-]+:.*## ' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*## "}; {printf " make %-14s %s\n", $$1, $$2}'
|
||
|
||
tools: ## 安裝 goctl、goimports、golangci-lint(需 Go,且 GOPATH/bin 在 PATH)
|
||
@command -v $(GOCTL) >/dev/null 2>&1 || (echo ">> installing goctl" && $(GO) install $(GOCTL_PKG))
|
||
@command -v goimports >/dev/null 2>&1 || (echo ">> installing goimports" && $(GO) install golang.org/x/tools/cmd/goimports@latest)
|
||
@if ! command -v golangci-lint >/dev/null 2>&1 || ! golangci-lint version 2>/dev/null | grep -q 'version 2\.'; then \
|
||
echo ">> installing golangci-lint v2"; \
|
||
$(GO) install $(GOLANGCI_PKG); \
|
||
fi
|
||
@echo "tools OK"
|
||
@echo " goctl: $$(goctl --version 2>/dev/null || echo missing)"
|
||
@echo " golangci-lint: $$(golangci-lint version 2>/dev/null | head -1 || echo missing)"
|
||
|
||
gen-api: tools ## 由 .api 生成 handler / logic / types(自訂 handler 模板)
|
||
$(GOCTL) api go -api $(API_ENTRY) -dir . -style $(GO_ZERO_STYLE) -home generate/goctl
|
||
|
||
gen-mock: ## 依 go:generate 產生 internal/model/*/mock(gomock)
|
||
$(GO) generate ./internal/model/...
|
||
|
||
build-go-doc: ## 編譯 go-doc(OpenAPI 文件生成器)
|
||
@echo ">> building $(GO_DOC_BIN)"
|
||
@mkdir -p $(GO_DOC_DIR)/bin
|
||
@cd $(GO_DOC_DIR) && $(GO) build -o bin/go-doc ./cmd/go-doc
|
||
|
||
gen-doc: build-go-doc ## 從 .api 生成 OpenAPI 3.0 YAML
|
||
@mkdir -p $(DOC_OUT)
|
||
$(GO_DOC_BIN) -a $(API_ENTRY) -d $(DOC_OUT) -f gateway -s openapi3.0 -y
|
||
@echo "Generated: $(DOC_OUT)/gateway.yaml"
|
||
|
||
test: ## 執行測試
|
||
$(GO) test ./...
|
||
fmt: ## gofmt + goimports(不含 lint)
|
||
$(GOFMT) -s -w $(GOFILES)
|
||
@command -v goimports >/dev/null 2>&1 && goimports -w . || (echo "goimports not found; run: make tools" && exit 1)
|
||
|
||
lint: tools ## golangci-lint 靜態檢查
|
||
golangci-lint run ./...
|
||
|
||
lint-fix: tools ## 自動修正可修的 lint / formatter 問題(見 .golangci.yml)
|
||
golangci-lint run --fix ./...
|
||
|
||
fix: fmt lint-fix lint ## 格式化 + 自動修 lint + 再檢查(提交前建議)
|
||
|
||
check: fix test ## 提交 / PR 前完整檢查(fmt、lint、test)
|
||
|
||
# ============================================================
|
||
# Docker compose(本機依賴)
|
||
# ============================================================
|
||
|
||
DOCKER_COMPOSE ?= docker compose
|
||
COMPOSE_FILE := deploy/docker-compose.yml
|
||
COMPOSE := $(DOCKER_COMPOSE) -f $(COMPOSE_FILE)
|
||
|
||
deps-up: ## 起 Mongo + Redis(最小本機依賴)
|
||
$(COMPOSE) up -d mongo redis
|
||
|
||
deps-up-smtp: ## 起 Mongo + Redis + MailHog
|
||
$(COMPOSE) --profile smtp up -d mongo redis mailhog
|
||
|
||
deps-down: ## 停服務(保留 volume)
|
||
$(COMPOSE) --profile smtp --profile k6 --profile ldap down
|
||
|
||
deps-down-v: ## 停服務並刪 volume(會清資料)
|
||
$(COMPOSE) --profile smtp --profile k6 --profile ldap down -v
|
||
|
||
deps-logs: ## 看 compose log
|
||
$(COMPOSE) --profile smtp --profile k6 --profile ldap logs -f
|
||
|
||
# OpenLDAP(本機 LDAP 測試,見 deploy/openldap/)
|
||
LDAP_COMPOSE := $(DOCKER_COMPOSE) -f deploy/openldap/docker-compose.yml
|
||
|
||
ldap-up: ## 只起 OpenLDAP(profile ldap;ZITADEL 用 ldap://openldap:389)
|
||
$(COMPOSE) --profile ldap up -d openldap
|
||
@echo "→ run 'make ldap-wait' to seed alice/bob"
|
||
@echo "→ 測試帳號見 deploy/openldap/README.md"
|
||
|
||
ldap-down: ## 停 OpenLDAP
|
||
$(COMPOSE) --profile ldap stop openldap
|
||
|
||
ldap-wait: ## 等 OpenLDAP ready 並 seed alice/bob
|
||
@$(MAKE) -s k6-wait-ldap
|
||
|
||
# ============================================================
|
||
# k6 測試(test/k6/)
|
||
# ============================================================
|
||
|
||
K6 ?= k6
|
||
K6_GATEWAY_CONFIG := etc/gateway.k6.yaml
|
||
K6_GATEWAY_BIN := bin/gateway-k6
|
||
K6_DIR := test/k6
|
||
K6_PAT_FILE := deploy/zitadel/machinekey/zitadel-admin-sa.token
|
||
K6_ENV_FILE := deploy/zitadel/machinekey/k6.env
|
||
K6_BOOTSTRAP_ENV := deploy/zitadel/machinekey/dev-bootstrap.env
|
||
ZITADEL_HEALTH_URL := http://localhost:8080/debug/healthz
|
||
|
||
# k6 安裝指引(macOS / Linux)
|
||
define K6_INSTALL_HINT
|
||
k6 not found in PATH.
|
||
|
||
Install:
|
||
macOS (Homebrew): brew install k6
|
||
Linux (apt): sudo gpg -k && sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 \
|
||
&& echo 'deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main' | sudo tee /etc/apt/sources.list.d/k6.list \
|
||
&& sudo apt update && sudo apt install k6
|
||
Other: https://grafana.com/docs/k6/latest/set-up/install-k6/
|
||
|
||
Or use docker (no install): K6='docker run --rm -i --network host -v $$PWD:/app -w /app grafana/k6:latest' make k6-smoke
|
||
endef
|
||
export K6_INSTALL_HINT
|
||
|
||
k6-check: ## 檢查 k6 是否安裝(沒裝會印 install 指引)
|
||
@command -v $(K6) >/dev/null 2>&1 || (echo "$$K6_INSTALL_HINT"; exit 1)
|
||
@echo "k6: $$($(K6) version 2>&1 | head -1)"
|
||
|
||
k6-up: ## 起 k6 全棧(mongo + redis + mailhog + postgres + openldap + zitadel)
|
||
$(COMPOSE) --profile k6 up -d mongo redis mailhog postgres openldap zitadel
|
||
@echo "OpenLDAP + ZITADEL bootstrapping (ZITADEL 首次約 30–90s)…"
|
||
@echo "→ run 'make k6-wait' to block until ready"
|
||
@echo "→ LDAP 測試帳號見 deploy/openldap/README.md(alice / Password1!)"
|
||
|
||
k6-wait-ldap: ## 等 OpenLDAP ready 並 seed alice/bob
|
||
@echo "waiting for OpenLDAP (gateway-openldap)…"
|
||
@for i in $$(seq 1 90); do \
|
||
if docker exec gateway-openldap ldapsearch -x -H ldap://localhost \
|
||
-b "dc=gateway,dc=local" \
|
||
-D "cn=admin,dc=gateway,dc=local" -w admin -LLL -s base "(objectClass=*)" dn 2>/dev/null | grep -q 'dc=gateway'; then \
|
||
echo "openldap ready ($$i s)"; \
|
||
$(MAKE) -s ldap-seed; exit 0; \
|
||
fi; \
|
||
sleep 1; \
|
||
done; \
|
||
echo "openldap did not become ready in 90s — check: docker logs gateway-openldap"; exit 1
|
||
|
||
ldap-seed: ## 將 deploy/openldap/bootstrap 測試帳號寫入目錄(可重複執行)
|
||
@docker cp deploy/openldap/bootstrap/10-people.ldif gateway-openldap:/tmp/10-people.ldif
|
||
@docker exec gateway-openldap ldapadd -x -H ldap://localhost \
|
||
-D "cn=admin,dc=gateway,dc=local" -w admin -c -f /tmp/10-people.ldif 2>&1 \
|
||
| grep -Ev '^(ldap_add: Already exists|adding new entry)' || true
|
||
@echo "ldap seed done (alice / bob @ ou=people,dc=gateway,dc=local)"
|
||
|
||
ldap-test: ## 列出 LDAP 測試使用者 alice / bob
|
||
@docker exec gateway-openldap ldapsearch -x -H ldap://localhost \
|
||
-b "ou=people,dc=gateway,dc=local" \
|
||
-D "cn=admin,dc=gateway,dc=local" -w admin \
|
||
"(|(uid=alice)(uid=bob))" uid mail cn
|
||
|
||
k6-bootstrap-zitadel: ## 自動建立 ZITADEL LDAP IdP + OIDC App(dev / k6)
|
||
@if [ ! -s "$(K6_PAT_FILE)" ]; then echo "PAT missing — run 'make k6-wait' first"; exit 1; fi
|
||
@python3 deploy/zitadel/bootstrap_dev.py
|
||
@echo "→ OAuth / LDAP IdP 寫入 $(K6_BOOTSTRAP_ENV)"
|
||
|
||
k6-wait: k6-wait-ldap ## 等 OpenLDAP + ZITADEL ready + bootstrap IdP/OAuth + 寫 k6.env
|
||
@echo "waiting for ZITADEL at $(ZITADEL_HEALTH_URL)…"
|
||
@for i in $$(seq 1 120); do \
|
||
if curl -fsS $(ZITADEL_HEALTH_URL) >/dev/null 2>&1; then \
|
||
echo "zitadel ready ($$i s)"; break; \
|
||
fi; \
|
||
sleep 1; \
|
||
if [ $$i -eq 120 ]; then echo "zitadel did not become ready in 120s"; exit 1; fi; \
|
||
done
|
||
@for i in $$(seq 1 30); do \
|
||
if [ -s "$(K6_PAT_FILE)" ]; then break; fi; \
|
||
sleep 1; \
|
||
done
|
||
@if [ ! -s "$(K6_PAT_FILE)" ]; then \
|
||
echo "PAT file $(K6_PAT_FILE) missing — check 'docker logs gateway-zitadel'"; \
|
||
exit 1; \
|
||
fi
|
||
@$(MAKE) -s k6-bootstrap-zitadel
|
||
@PAT=$$(tr -d '\n' < $(K6_PAT_FILE)); \
|
||
printf 'export ZITADEL_SERVICE_TOKEN=%s\nexport BASE_URL=http://localhost:8888\nexport MAILHOG_URL=http://localhost:8025\nexport REDIS_ADDR=localhost:6379\n' "$$PAT" > $(K6_ENV_FILE); \
|
||
if [ -s "$(K6_BOOTSTRAP_ENV)" ]; then cat $(K6_BOOTSTRAP_ENV) >> $(K6_ENV_FILE); fi; \
|
||
echo "wrote $(K6_ENV_FILE)"
|
||
@$(MAKE) -s k6-seed-fixtures
|
||
@echo "tip: 'source $(K6_ENV_FILE)' to load into your shell"
|
||
@echo "tip: run 'make dev-restart-gateway' if gateway was already running"
|
||
|
||
# k6-seed-fixtures idempotently upserts the k6-tenant + K6INVITE invite code
|
||
# into gateway_k6 so /auth/register resolves successfully. The invite hash is
|
||
# sha256("K6INVITE") — see internal/model/auth/domain/const.go HashInviteCode.
|
||
# Uses mongosh inside the container; no extra Go binary needed.
|
||
K6_TENANT_ID := k6-tenant
|
||
K6_INVITE_HASH := e62a291f0dcf88c50c91fdc78e3539c55e1e20ef645c640b1ec778fe4fabb4cb
|
||
k6-seed-fixtures: ## upsert k6-tenant + K6INVITE 到 mongo
|
||
@docker exec gateway-mongo mongosh --quiet --eval '\
|
||
db = db.getSiblingDB("gateway_k6"); \
|
||
var now = NumberLong(Date.now()); \
|
||
var t = db.tenants.updateOne( \
|
||
{tenant_id: "$(K6_TENANT_ID)"}, \
|
||
{$$set: {tenant_id: "$(K6_TENANT_ID)", slug: "$(K6_TENANT_ID)", name: "k6 Tenant", uid_prefix: "K6", status: "active", update_at: now}, $$setOnInsert: {create_at: now}}, \
|
||
{upsert: true} \
|
||
); \
|
||
var i = db.invite_codes.updateOne( \
|
||
{tenant_id: "$(K6_TENANT_ID)", code_hash: "$(K6_INVITE_HASH)"}, \
|
||
{$$set: {tenant_id: "$(K6_TENANT_ID)", code_hash: "$(K6_INVITE_HASH)", max_uses: NumberLong(1000000), expires_at: NumberLong(0), new_users_only: false, update_at: now}, $$setOnInsert: {used_count: NumberLong(0), create_at: now}}, \
|
||
{upsert: true} \
|
||
); \
|
||
print("tenant matched=" + t.matchedCount + " upserted=" + (t.upsertedId?1:0) + ", invite matched=" + i.matchedCount + " upserted=" + (i.upsertedId?1:0));' && \
|
||
echo "seeded fixtures (tenant=$(K6_TENANT_ID) invite=K6INVITE) into gateway_k6"
|
||
|
||
# Back-compat alias
|
||
k6-seed-tenant: k6-seed-fixtures ## (alias for k6-seed-fixtures)
|
||
|
||
k6-build: ## 建 gateway binary 給 k6 使用
|
||
@mkdir -p $(dir $(K6_GATEWAY_BIN))
|
||
$(GO) build -o $(K6_GATEWAY_BIN) ./gateway.go
|
||
|
||
k6-gateway: k6-build ## 前景啟 gateway(吃 etc/gateway.k6.yaml + ZITADEL env)
|
||
@if [ ! -s "$(K6_ENV_FILE)" ]; then echo "run 'make k6-wait' first"; exit 1; fi
|
||
@set -a; . $(K6_ENV_FILE); set +a; \
|
||
$(K6_GATEWAY_BIN) -f $(K6_GATEWAY_CONFIG)
|
||
|
||
k6-smoke: k6-check ## 跑 smoke 測試(每個端點一發)
|
||
@if [ ! -s "$(K6_ENV_FILE)" ]; then echo "run 'make k6-wait' first"; exit 1; fi
|
||
@set -a; . $(K6_ENV_FILE); set +a; \
|
||
for f in $(K6_DIR)/smoke/*.js; do \
|
||
echo "==> $$f"; $(K6) run "$$f" || exit 1; \
|
||
done
|
||
|
||
k6-journey: k6-check ## 跑 journey 測試(多步驟流程)
|
||
@if [ ! -s "$(K6_ENV_FILE)" ]; then echo "run 'make k6-wait' first"; exit 1; fi
|
||
@set -a; . $(K6_ENV_FILE); set +a; \
|
||
for f in $(K6_DIR)/journeys/*.js; do \
|
||
echo "==> $$f"; $(K6) run "$$f" || exit 1; \
|
||
done
|
||
|
||
k6-all: k6-smoke k6-journey ## smoke + journey
|
||
|
||
k6-seed-admin: k6-build ## 註冊 k6-admin 並 seed tenant_admin role(rbac journey 用)
|
||
@if [ ! -s "$(K6_ENV_FILE)" ]; then echo "run 'make k6-wait' first"; exit 1; fi
|
||
@mkdir -p bin
|
||
$(GO) build -o bin/k6-seed-admin ./cmd/k6-seed-admin
|
||
@set -a; . $(K6_ENV_FILE); set +a; \
|
||
./bin/k6-seed-admin > $(K6_ENV_FILE).admin || (echo "k6-seed-admin failed"; exit 1); \
|
||
cat $(K6_ENV_FILE).admin >> $(K6_ENV_FILE); \
|
||
echo "admin credentials appended to $(K6_ENV_FILE):"; \
|
||
cat $(K6_ENV_FILE).admin
|
||
|
||
k6-down: ## 停 k6 stack 並清 volume(會清 Postgres / Mongo / Redis 資料)
|
||
$(COMPOSE) --profile k6 down -v
|
||
@rm -f $(K6_PAT_FILE) $(K6_ENV_FILE) $(K6_ENV_FILE).admin $(K6_ENV_FILE).tmp deploy/zitadel/machinekey/zitadel-admin-sa.json
|
||
@echo "k6 stack stopped, volumes & PAT removed"
|
||
|
||
# ============================================================
|
||
# 一鍵本機測試環境(Docker + Gateway + 種子資料)
|
||
# ============================================================
|
||
|
||
DEV_DIR := .dev
|
||
DEV_GATEWAY_PID := $(DEV_DIR)/gateway.pid
|
||
DEV_GATEWAY_LOG := $(DEV_DIR)/gateway.log
|
||
|
||
.PHONY: dev-up dev-down dev-status dev-restart-gateway
|
||
|
||
dev-up: k6-up k6-wait k6-build dev-restart-gateway ## 一鍵起全套:mongo/redis/mailhog/zitadel + seed + Gateway 背景
|
||
@echo ""
|
||
@echo "=========================================="
|
||
@echo " 本機測試環境已就緒"
|
||
@echo "=========================================="
|
||
@echo " Gateway API http://localhost:8888"
|
||
@echo " 前端(另開終端) make frontend-dev → http://localhost:5173"
|
||
@echo " MailHog 收信 http://localhost:8025 (註冊 OTP 在這裡看)"
|
||
@echo " ZITADEL 主控台 http://localhost:8080/ui/console"
|
||
@echo ""
|
||
@echo " 註冊預設:租戶 k6-tenant · 邀請碼 K6INVITE"
|
||
@echo " LDAP 登入:alice / Password1!(make k6-wait 已自動設定 ZITADEL IdP)"
|
||
@echo ""
|
||
@echo " 管理後台:make dev-seed-admin 後用輸出的帳密登入"
|
||
@echo " 關閉環境:make dev-down"
|
||
@echo " 查看狀態:make dev-status"
|
||
@echo " OAuth/LDAP 設定變更後:make dev-restart-gateway"
|
||
@echo "=========================================="
|
||
|
||
dev-seed-admin: k6-seed-admin ## 建立具 tenant_admin 的管理員(dev-up 之後執行)
|
||
|
||
dev-down: ## 停 Gateway 背景行程 + k6 docker stack
|
||
@if [ -f $(DEV_GATEWAY_PID) ]; then \
|
||
pid=$$(cat $(DEV_GATEWAY_PID)); \
|
||
if kill -0 $$pid 2>/dev/null; then kill $$pid && echo "stopped gateway (pid $$pid)"; fi; \
|
||
rm -f $(DEV_GATEWAY_PID); \
|
||
fi
|
||
@$(MAKE) -s k6-down
|
||
@rm -rf $(DEV_DIR)
|
||
@echo "dev environment stopped"
|
||
|
||
dev-status: ## 顯示 docker / gateway / health 狀態
|
||
@echo "=== docker (k6 profile) ==="
|
||
@$(COMPOSE) --profile k6 ps 2>/dev/null || true
|
||
@echo ""
|
||
@echo "=== gateway :8888 ==="
|
||
@if [ -f $(DEV_GATEWAY_PID) ] && kill -0 $$(cat $(DEV_GATEWAY_PID)) 2>/dev/null; then \
|
||
echo "running pid $$(cat $(DEV_GATEWAY_PID))"; \
|
||
curl -fsS http://localhost:8888/api/v1/health 2>/dev/null | head -c 200 || echo "(health check failed)"; \
|
||
echo ""; \
|
||
else \
|
||
echo "not running (run: make dev-up)"; \
|
||
fi
|
||
|
||
dev-restart-gateway: k6-build ## 重啟 Gateway(載入 k6.env 的 OAuth / LDAP 設定)
|
||
@if [ ! -s "$(K6_ENV_FILE)" ]; then echo "run 'make k6-wait' first"; exit 1; fi
|
||
@if [ -f $(DEV_GATEWAY_PID) ]; then \
|
||
pid=$$(cat $(DEV_GATEWAY_PID)); \
|
||
kill -0 $$pid 2>/dev/null && kill $$pid || true; \
|
||
rm -f $(DEV_GATEWAY_PID); \
|
||
fi
|
||
@if command -v lsof >/dev/null 2>&1; then \
|
||
orphan=$$(lsof -ti :8888 2>/dev/null || true); \
|
||
if [ -n "$$orphan" ]; then kill $$orphan 2>/dev/null || true; sleep 1; fi; \
|
||
fi
|
||
@mkdir -p $(DEV_DIR)
|
||
@set -a; . $(K6_ENV_FILE); set +a; \
|
||
nohup $(K6_GATEWAY_BIN) -f $(K6_GATEWAY_CONFIG) > $(DEV_GATEWAY_LOG) 2>&1 & \
|
||
echo $$! > $(DEV_GATEWAY_PID); \
|
||
echo "gateway restarted (pid $$(cat $(DEV_GATEWAY_PID)))"
|
||
@for i in $$(seq 1 30); do \
|
||
if curl -fsS http://localhost:8888/api/v1/health >/dev/null 2>&1; then \
|
||
echo "gateway ready ($$i s)"; exit 0; \
|
||
fi; \
|
||
sleep 1; \
|
||
done; \
|
||
echo "gateway did not become ready — tail $(DEV_GATEWAY_LOG)"; tail -20 $(DEV_GATEWAY_LOG); exit 1
|
||
|
||
# ============================================================
|
||
# Frontend(使用者前台 + 管理後台)
|
||
# ============================================================
|
||
|
||
frontend-install: ## 安裝 frontend 依賴
|
||
cd frontend && npm install
|
||
|
||
frontend-dev: frontend-install ## 啟動前端 dev server(:5173,proxy /api → :8888)
|
||
cd frontend && npm run dev
|
||
|
||
frontend-build: frontend-install ## 建置前端靜態檔 → frontend/dist/
|
||
cd frontend && npm run build
|