package c_middle const CmiiEmqxTemplate = ` --- # ============== Secret - 密码管理 ============== apiVersion: v1 kind: Secret metadata: name: emqx-credentials namespace: {{ .Namespace }} labels: cmii.type: middleware cmii.app: helm-emqxs app.kubernetes.io/managed-by: octopus-control app.kubernetes.io/version: {{ .TagVersion }} type: Opaque stringData: # Dashboard管理员密码 dashboard-admin-password: "{{ .EmqxPassword }}" # MQTT用户密码 mqtt-admin-password: "{{ .EmqxPassword }}" --- # ============== ServiceAccount ============== apiVersion: v1 kind: ServiceAccount metadata: name: helm-emqxs namespace: {{ .Namespace }} --- # ============== Role - RBAC ============== apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: helm-emqxs namespace: {{ .Namespace }} rules: - apiGroups: [""] resources: - endpoints - pods verbs: - get - watch - list --- # ============== RoleBinding ============== apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: helm-emqxs namespace: {{ .Namespace }} subjects: - kind: ServiceAccount name: helm-emqxs namespace: {{ .Namespace }} roleRef: kind: Role name: helm-emqxs apiGroup: rbac.authorization.k8s.io --- # ============== ConfigMap - Bootstrap配置文件 ============== apiVersion: v1 kind: ConfigMap metadata: name: emqx-bootstrap-config namespace: {{ .Namespace }} labels: cmii.type: middleware cmii.app: helm-emqxs data: # 主配置文件 - 覆盖默认配置 emqx.conf: | # 节点配置 node { name = "emqx@${POD_NAME}.helm-emqxs-headless.{{ .Namespace }}.svc.cluster.local" cookie = "emqx-cluster-cookie-secret" data_dir = "/opt/emqx/data" } # 集群配置 cluster { name = emqxcl # 单节点 建议为 manual 多节点为k8s discovery_strategy = manual k8s { apiserver = "https://kubernetes.default.svc.cluster.local:443" service_name = "helm-emqxs-headless" # 这里可以改为 hostname address_type = dns namespace = "{{ .Namespace }}" suffix = "svc.cluster.local" } } # 日志配置 log { console { enable = true level = info } file { enable = true level = warning path = "/opt/emqx/log" } } # Dashboard配置 dashboard { listeners.http { bind = "0.0.0.0:18083" } default_username = "admin" default_password = "public" } # 监听器配置 listeners.tcp.default { bind = "0.0.0.0:1883" max_connections = 1024000 } listeners.ws.default { bind = "0.0.0.0:8083" max_connections = 1024000 websocket.mqtt_path = "/mqtt" } listeners.ssl.default { bind = "0.0.0.0:8883" max_connections = 512000 } # 认证配置 - 使用内置数据库 authentication = [ { mechanism = password_based backend = built_in_database user_id_type = username password_hash_algorithm { name = sha256 salt_position = suffix } # Bootstrap文件路径 - 用于初始化用户 bootstrap_file = "/opt/emqx/data/bootstrap_users.json" bootstrap_type = plain } ] # 授权配置 authorization { no_match = deny deny_action = disconnect sources = [ { type = built_in_database enable = true } ] } # MQTT协议配置 mqtt { max_packet_size = "1MB" max_clientid_len = 65535 max_topic_levels = 128 max_qos_allowed = 2 max_topic_alias = 65535 retain_available = true wildcard_subscription = true shared_subscription = true } --- # ============== ConfigMap - Users & ACL (严格 JSON 格式) ============== apiVersion: v1 kind: ConfigMap metadata: name: emqx-bootstrap-users namespace: {{ .Namespace }} data: bootstrap_users.json: | [ { "user_id": "admin", "password": "{{ .EmqxPassword }}", "is_superuser": true }, { "user_id": "cmlc", "password": "{{ .EmqxPassword }}", "is_superuser": false } ] # 【修改点】既然有jq,这里使用标准的 JSON 数组格式,最不容易出错 bootstrap_acl.json: | [ { "username": "admin", "rules": [ {"action": "all", "permission": "allow", "topic": "#"} ] }, { "username": "cmlc", "rules": [ {"action": "publish", "permission": "allow", "topic": "#"}, {"action": "subscribe", "permission": "allow", "topic": "#"} ] } ] --- # ============== ConfigMap - 初始化脚本 (修正版) ============== apiVersion: v1 kind: ConfigMap metadata: name: emqx-init-dashboard namespace: {{ .Namespace }} data: init-dashboard.sh: | #!/bin/bash set -e DASHBOARD_USER="admin" DASHBOARD_PASS="${DASHBOARD_ADMIN_PASSWORD}" EMQX_API="http://localhost:18083/api/v5" ACL_FILE="/bootstrap/bootstrap_acl.json" # 辅助函数:打印带时间戳的日志 log() { echo "[$(date +'%H:%M:%S')] $1" } log "======================================" log "初始化 Dashboard 与 ACL (Debug Version)" log "======================================" # ---------------------------------------------------------------- # 1. 等待 EMQX API 就绪 # ---------------------------------------------------------------- log "[1/4] 等待 EMQX API 就绪..." for i in $(seq 1 60); do if curl -s -f -m 5 "${EMQX_API}/status" > /dev/null 2>&1; then log "✓ EMQX API 已就绪" break fi if [ $i -eq 60 ]; then log "✗ EMQX API 启动超时" exit 1 fi sleep 5 done # ---------------------------------------------------------------- # 2. 修改 Dashboard 密码 # ---------------------------------------------------------------- log "[2/4] 检查/更新 Dashboard 密码..." # 获取 Token (尝试默认密码) LOGIN_RESP=$(curl -s -X POST "${EMQX_API}/login" \ -H 'Content-Type: application/json' \ -d "{\"username\":\"${DASHBOARD_USER}\",\"password\":\"public\"}") TOKEN=$(echo "$LOGIN_RESP" | jq -r '.token // empty') if [ -n "$TOKEN" ]; then log " 检测到默认密码,正在更新..." curl -s -f -X POST "${EMQX_API}/users/${DASHBOARD_USER}/change_pwd" \ -H "Authorization: Bearer ${TOKEN}" \ -H 'Content-Type: application/json' \ -d "{\"old_pwd\":\"public\",\"new_pwd\":\"${DASHBOARD_PASS}\"}" log " ✓ Dashboard 密码已更新" else log " ℹ 无法使用默认密码登录,跳过更新(可能已修改)" fi # ---------------------------------------------------------------- # 3. 导入 ACL 规则 # ---------------------------------------------------------------- echo "[3/3] 导入ACL规则..." # 重新登录获取最新 Token LOGIN_RESP=$(curl -sS -X POST "${EMQX_API}/login" \ -H 'Content-Type: application/json' \ -d "{\"username\":\"${DASHBOARD_USER}\",\"password\":\"${DASHBOARD_PASS}\"}") TOKEN=$(echo "$LOGIN_RESP" | jq -r '.token // empty') if [ -z "$TOKEN" ]; then echo " ✗ 无法获取Token,请检查密码设置" exit 0 fi if [ -f "$ACL_FILE" ]; then echo " 正在解析 ACL 文件: $ACL_FILE" if ! jq -e . "$ACL_FILE" >/dev/null 2>&1; then echo " ✗ ACL 文件 JSON 格式错误,跳过处理" exit 0 fi jq -c '.[]' "$ACL_FILE" | while read -r user_config; do USERNAME=$(echo "$user_config" | jq -r '.username // empty') # ✅ PUT/POST 都需要 username + rules(username 是 required) REQ_BODY=$(echo "$user_config" | jq -c '{username: .username, rules: .rules}') if [ -z "$USERNAME" ]; then echo " ✗ ACL 条目缺少 username,跳过" continue fi echo " 配置用户 ${USERNAME} 的ACL规则..." # 1) 优先 PUT(覆盖更新) http_code=$(curl -sS -o /tmp/emqx_acl_resp.json -w '%{http_code}' \ -X PUT "${EMQX_API}/authorization/sources/built_in_database/rules/users/${USERNAME}" \ -H "Authorization: Bearer ${TOKEN}" \ -H 'Content-Type: application/json' \ -d "$REQ_BODY") if [ "$http_code" = "204" ]; then echo " ✓ PUT 更新成功" elif [ "$http_code" = "404" ]; then # 2) 不存在则 POST 创建 http_code2=$(curl -sS -o /tmp/emqx_acl_resp.json -w '%{http_code}' \ -X POST "${EMQX_API}/authorization/sources/built_in_database/rules/users" \ -H "Authorization: Bearer ${TOKEN}" \ -H 'Content-Type: application/json' \ -d "$REQ_BODY") if [ "$http_code2" = "204" ]; then echo " ✓ POST 创建成功" else echo " ✗ POST 失败 (HTTP ${http_code2}):$(cat /tmp/emqx_acl_resp.json 2>/dev/null || true)" exit 1 fi else echo " ✗ PUT 失败 (HTTP ${http_code}):$(cat /tmp/emqx_acl_resp.json 2>/dev/null || true)" exit 1 fi # 3) 导入后验证(可选但强烈建议保留) verify_code=$(curl -sS -o /tmp/emqx_acl_verify.json -w '%{http_code}' \ -H "Authorization: Bearer ${TOKEN}" \ "${EMQX_API}/authorization/sources/built_in_database/rules/users/${USERNAME}") if [ "$verify_code" = "200" ]; then echo " ✓ 验证成功:$(cat /tmp/emqx_acl_verify.json | jq -c '.')" else echo " ✗ 验证失败 (HTTP ${verify_code}):$(cat /tmp/emqx_acl_verify.json 2>/dev/null || true)" exit 1 fi done echo " ✓ ACL 规则导入完成" else echo " ℹ 未找到 ACL 文件" fi --- # ============== StatefulSet ============== apiVersion: apps/v1 kind: StatefulSet metadata: name: helm-emqxs namespace: {{ .Namespace }} labels: cmii.type: middleware cmii.app: helm-emqxs cmii.emqx.architecture: cluster helm.sh/chart: emqx-1.1.0 app.kubernetes.io/managed-by: octopus-control app.kubernetes.io/version: {{ .TagVersion }} spec: replicas: 1 serviceName: helm-emqxs-headless podManagementPolicy: Parallel updateStrategy: type: RollingUpdate selector: matchLabels: cmii.type: middleware cmii.app: helm-emqxs cmii.emqx.architecture: cluster template: metadata: labels: cmii.type: middleware cmii.app: helm-emqxs cmii.emqx.architecture: cluster helm.sh/chart: emqx-1.1.0 app.kubernetes.io/managed-by: octopus-control app.kubernetes.io/version: {{ .TagVersion }} spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: uavcloud.env operator: In values: {{- if .TenantEnv }} - {{ .TenantEnv }} {{- else }} - {{ .Namespace }} {{- end }} podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: cmii.app operator: In values: - helm-emqxs topologyKey: kubernetes.io/hostname imagePullSecrets: - name: harborsecret serviceAccountName: helm-emqxs securityContext: fsGroup: 1000 runAsUser: 1000 # InitContainer - 准备bootstrap文件 initContainers: - name: prepare-bootstrap # 动态选择 tools 镜像 {{- if .HarborPort }} image: {{ .HarborIPOrCustomImagePrefix }}:{{ .HarborPort }}/cmii/tools:1.0 {{- else }} image: {{ .HarborIPOrCustomImagePrefix }}cmii/tools:1.0 {{- end }} imagePullPolicy: IfNotPresent # ========================================================= # 权限: 必须以 root 身份运行才能 chown # ========================================================= securityContext: runAsUser: 0 command: - /bin/sh - -c - | echo "准备bootstrap文件..." # 创建数据目录 mkdir -p /opt/emqx/data # 复制bootstrap文件到数据目录 # 只在文件不存在时复制,避免覆盖已有数据 if [ ! -f /opt/emqx/data/bootstrap_users.json ]; then cp /bootstrap-src/bootstrap_users.json /opt/emqx/data/ echo "✓ 已复制用户bootstrap文件" else echo "ℹ 用户bootstrap文件已存在,跳过" fi # 设置权限 (现在有root权限,可以成功) chown -R 1000:1000 /opt/emqx/data echo "✓ Bootstrap准备完成" volumeMounts: - name: emqx-data mountPath: /opt/emqx/data - name: bootstrap-users mountPath: /bootstrap-src containers: # 主容器 - EMQX - name: emqx # 动态选择 emqx 镜像 {{- if .HarborPort }} image: {{ .HarborIPOrCustomImagePrefix }}:{{ .HarborPort }}/cmii/emqx:5.8.8 {{- else }} image: {{ .HarborIPOrCustomImagePrefix }}emqx:5.8.8 {{- end }} imagePullPolicy: IfNotPresent env: # Pod信息 - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: EMQX_DATA_DIR value: "/opt/emqx/data" ports: - name: mqtt containerPort: 1883 - name: mqttssl containerPort: 8883 - name: ws containerPort: 8083 - name: dashboard containerPort: 18083 - name: ekka containerPort: 4370 resources: requests: cpu: "500m" memory: "512Mi" limits: cpu: "2000m" memory: "2Gi" livenessProbe: httpGet: path: /status port: 18083 initialDelaySeconds: 60 periodSeconds: 30 timeoutSeconds: 10 failureThreshold: 3 readinessProbe: httpGet: path: /status port: 18083 initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 startupProbe: httpGet: path: /status port: 18083 initialDelaySeconds: 10 periodSeconds: 5 failureThreshold: 30 volumeMounts: - name: emqx-data mountPath: /opt/emqx/data # 使用 subPath 挂载单个配置文件,避免覆盖目录 - name: bootstrap-config mountPath: /opt/emqx/etc/emqx.conf subPath: emqx.conf # Sidecar - 初始化Dashboard密码和ACL - name: init-dashboard # 动态选择 tools 镜像 {{- if .HarborPort }} image: {{ .HarborIPOrCustomImagePrefix }}:{{ .HarborPort }}/cmii/tools:1.0 {{- else }} image: {{ .HarborIPOrCustomImagePrefix }}cmii/tools:1.0 {{- end }} imagePullPolicy: IfNotPresent command: - /bin/sh - -c - | # 等待主容器启动 echo "等待EMQX启动..." sleep 20 # 执行初始化 /bin/sh /scripts/init-dashboard.sh # 保持运行 echo "初始化完成,进入守护模式..." while true; do sleep 3600; done env: - name: DASHBOARD_ADMIN_PASSWORD valueFrom: secretKeyRef: name: emqx-credentials key: dashboard-admin-password resources: requests: cpu: "100m" memory: "64Mi" limits: cpu: "200m" memory: "128Mi" volumeMounts: - name: init-script mountPath: /scripts - name: bootstrap-users mountPath: /bootstrap volumes: - name: bootstrap-config configMap: name: emqx-bootstrap-config - name: bootstrap-users configMap: name: emqx-bootstrap-users - name: init-script configMap: name: emqx-init-dashboard defaultMode: 0755 - name: emqx-data persistentVolumeClaim: claimName: helm-emqxs --- # ============== Service - Headless ============== apiVersion: v1 kind: Service metadata: name: helm-emqxs-headless namespace: {{ .Namespace }} labels: cmii.type: middleware cmii.app: helm-emqxs cmii.emqx.architecture: cluster helm.sh/chart: emqx-1.1.0 app.kubernetes.io/managed-by: octopus-control app.kubernetes.io/version: {{ .TagVersion }} spec: type: ClusterIP clusterIP: None publishNotReadyAddresses: true selector: cmii.type: middleware cmii.app: helm-emqxs cmii.emqx.architecture: cluster ports: - name: mqtt port: 1883 targetPort: 1883 - name: mqttssl port: 8883 targetPort: 8883 - name: ws port: 8083 targetPort: 8083 - name: dashboard port: 18083 targetPort: 18083 - name: ekka port: 4370 targetPort: 4370 --- # ============== Service - NodePort ============== apiVersion: v1 kind: Service metadata: name: helm-emqxs namespace: {{ .Namespace }} labels: cmii.type: middleware cmii.app: helm-emqxs cmii.emqx.architecture: cluster helm.sh/chart: emqx-1.1.0 app.kubernetes.io/managed-by: octopus-control app.kubernetes.io/version: {{ .TagVersion }} spec: type: NodePort selector: cmii.type: middleware cmii.app: helm-emqxs cmii.emqx.architecture: cluster ports: - name: mqtt port: 1883 targetPort: 1883 nodePort: {{ .EmqxNodePort }} - name: dashboard port: 18083 targetPort: 18083 nodePort: {{ .EmqxDashboardNodePort }} - name: ws port: 8083 targetPort: 8083 nodePort: {{ .EmqxWebSocketNodePort }} - name: mqttssl port: 8883 targetPort: 8883 `