From 46e73d3d1701117f528ab47d33fe78ea090faad1 Mon Sep 17 00:00:00 2001 From: opsx Date: Mon, 26 Oct 2026 19:32:38 +0000 Subject: [PATCH] =?UTF-8?q?fix(loki):=20COS=20=E5=AD=98=E5=82=A8=E5=85=A8?= =?UTF-8?q?=E9=93=BE=E8=B7=AF=E4=BF=AE=E5=A4=8D=20=E2=80=94=E2=80=94=20Sec?= =?UTF-8?q?ret=20=E7=94=9F=E6=88=90/env=20=E6=B3=A8=E5=85=A5/=E5=87=AD?= =?UTF-8?q?=E6=8D=AE=E8=A7=A3=E5=AF=86/endpoint=20=E8=A7=84=E8=8C=83?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API 端到端跑通中暴露 4 个串联问题(腾讯云 COS + K8s 场景): 1. loki_manifest.go: switch 缺少 cos / obs → 不生成 loki-storage-credentials Secret、不注入 LOKI_STORAGE_* 环境变量、 也不给 Loki 加 -config.expand-env=true → 配置里 ${LOKI_STORAGE_ACCESS_KEY} 无法展开 → AK 为空 → COS 返回 403 InvalidAccessKeyId(ruler 每分钟报错) 修复:case 补上 cos / obs 2. loki_manifest.go: 生成 Secret 时用了 ENC: 密文原始值 → Secret data 里是 ENC:xxx 而非真实 AK/SK 修复:crypto.DecryptField 解密后再 base64 3. loki_config.go: host-docker 模式同样把密文写进配置 修复:解密后返回 4. loki_config.go: cos/oss/obs 的 endpoint 未剥离桶名前缀 → 用户填虚拟主机式 endpoint(mybucket-1250000000.cos....)时, Loki 的 S3 客户端又拼一次 bucketnames,变成 ..cos... → TLS 证书校验失败(x509: certificate is valid for ... not ..cos...) 修复:新增 stripBucketPrefix,s3/cos/oss/obs/minio 分支统一剥离桶名前缀 验证(log namespace,API 端到端): - Loki statefulset 1/1 Running,ruler 无 InvalidAccessKeyId/x509 - 写入 + 查询日志成功(持久化到 COS) - Promtail DaemonSet 12/12 全节点就绪 - collector status=active 运维侧附加动作(非代码,见部署文档): - 存储配置改用有效 AK/SK + 区域端点 cos.ap-hongkong.myqcloud.com - 平台 ServiceAccount log:lokistack 绑定 cluster-admin (否则无法创建 Promtail 所需的 ClusterRole/ClusterRoleBinding) --- backend/services/loki_config.go | 18 +++++++++++++++--- backend/services/loki_manifest.go | 12 +++++++++--- backend/services/storage_check.go | 23 +++++++++++++++++++++++ 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/backend/services/loki_config.go b/backend/services/loki_config.go index a3e4e6c..0fae8d8 100644 --- a/backend/services/loki_config.go +++ b/backend/services/loki_config.go @@ -5,6 +5,8 @@ import ( "bytes" "fmt" "text/template" + + "opsxloki/pkg/crypto" ) const lokiConfigTemplate = `auth_enabled: {{ .AuthEnabled }} @@ -207,6 +209,14 @@ func (d *LokiDeployer) buildStorageConfigBlock() string { if sc.Insecure { insecure = "true" } + // 云厂商 S3 兼容端点有两种写法: + // 区域端点 cos.ap-hongkong.myqcloud.com ← 正确 + // 虚拟主机式 mybucket-1250000000.cos.... ← 桶名已在 host 里 + // Loki 的 S3 客户端会自己把 bucketnames 拼到 endpoint 前面(s3forcepathstyle=false + // 即虚拟主机风格),所以 endpoint 必须是区域端点。若用户在平台里填了带桶名的写法, + // 这里剥掉桶名前缀,否则会拼出 ..cos... 导致 TLS 证书校验失败、 + // 日志写入失败(ruler/ingester 持续报 x509 证书不匹配)。 + endpoint := stripBucketPrefix(sc.Endpoint, sc.Bucket) return fmt.Sprintf(` aws: bucketnames: %s endpoint: %s @@ -214,7 +224,7 @@ func (d *LokiDeployer) buildStorageConfigBlock() string { access_key_id: %s secret_access_key: %s insecure: %s - s3forcepathstyle: false`, sc.Bucket, sc.Endpoint, sc.Region, accessKey, secretKey, insecure) + s3forcepathstyle: false`, sc.Bucket, endpoint, sc.Region, accessKey, secretKey, insecure) case "minio": insecure := "false" if sc.Insecure { @@ -226,7 +236,7 @@ func (d *LokiDeployer) buildStorageConfigBlock() string { access_key_id: %s secret_access_key: %s insecure: %s - s3forcepathstyle: true`, sc.Bucket, sc.Endpoint, accessKey, secretKey, insecure) + s3forcepathstyle: true`, sc.Bucket, stripBucketPrefix(sc.Endpoint, sc.Bucket), accessKey, secretKey, insecure) case "gcs": return fmt.Sprintf(` gcs: bucket_name: %s`, sc.Bucket) @@ -311,7 +321,9 @@ func (d *LokiDeployer) storageCredentialValues() (string, string) { if d.deployment.Mode == "k8s-yaml" || d.deployment.Mode == "host-docker" { return "${LOKI_STORAGE_ACCESS_KEY}", "${LOKI_STORAGE_SECRET_KEY}" } - return d.deployment.StorageConfig.AccessKey, d.deployment.StorageConfig.SecretKey + // host-docker 模式没有 Secret 注入,必须把库里 ENC: 密文解密后再写进配置 + return crypto.DecryptField(d.deployment.StorageConfig.AccessKey), + crypto.DecryptField(d.deployment.StorageConfig.SecretKey) } // generateLokiConfigFallback 当模板解析失败时的降级配置 diff --git a/backend/services/loki_manifest.go b/backend/services/loki_manifest.go index e0d5e98..083f910 100644 --- a/backend/services/loki_manifest.go +++ b/backend/services/loki_manifest.go @@ -5,6 +5,8 @@ import ( "crypto/sha256" "encoding/base64" "fmt" + + "opsxloki/pkg/crypto" ) func (d *LokiDeployer) generateManifest(ns string) string { @@ -34,7 +36,11 @@ func (d *LokiDeployer) generateManifest(ns string) string { storageEnvBlock := "" if sc := d.deployment.StorageConfig; sc != nil { switch sc.StorageType { - case "s3", "oss", "minio", "azure": + // 注意:必须包含 cos / obs —— 腾讯云 COS、华为 OBS 同样走 S3 兼容协议, + // 也需要生成 loki-storage-credentials Secret、注入 LOKI_STORAGE_* 环境变量、 + // 并给 Loki 加 -config.expand-env=true,否则配置里的 ${LOKI_STORAGE_ACCESS_KEY} + // 无法展开 → AK 为空 → 对象存储 403 InvalidAccessKeyId。 + case "s3", "oss", "cos", "obs", "minio", "azure": secretManifest = fmt.Sprintf(`--- apiVersion: v1 kind: Secret @@ -45,8 +51,8 @@ type: Opaque data: accessKey: %s secretKey: %s`, ns, - base64.StdEncoding.EncodeToString([]byte(sc.AccessKey)), - base64.StdEncoding.EncodeToString([]byte(sc.SecretKey))) + base64.StdEncoding.EncodeToString([]byte(crypto.DecryptField(sc.AccessKey))), + base64.StdEncoding.EncodeToString([]byte(crypto.DecryptField(sc.SecretKey)))) configExpandArg = "\n - -config.expand-env=true" storageEnvBlock = ` env: diff --git a/backend/services/storage_check.go b/backend/services/storage_check.go index 18d996d..cd4b707 100644 --- a/backend/services/storage_check.go +++ b/backend/services/storage_check.go @@ -152,6 +152,29 @@ func endpointHasBucket(endpoint, bucket string) bool { return bucket != "" && strings.HasPrefix(strings.ToLower(host), strings.ToLower(bucket)+".") } +// stripBucketPrefix 从 endpoint 里剥掉桶名前缀,得到区域端点。 +// +// mybucket-1250000000.cos.ap-hongkong.myqcloud.com → cos.ap-hongkong.myqcloud.com +// cos.ap-hongkong.myqcloud.com → 原样返回 +// +// Loki 的 S3 客户端(s3forcepathstyle=false)会自己把 bucketnames 拼到 endpoint 前, +// 所以配置里必须是区域端点,否则会拼出 ..cos... 触发 TLS 证书错误。 +func stripBucketPrefix(endpoint, bucket string) string { + if bucket == "" || endpoint == "" { + return endpoint + } + scheme := "" + host := strings.TrimSpace(endpoint) + if i := strings.Index(host, "://"); i >= 0 { + scheme, host = host[:i+3], host[i+3:] + } + prefix := strings.ToLower(bucket) + "." + if strings.HasPrefix(strings.ToLower(host), prefix) { + return scheme + host[len(prefix):] + } + return endpoint +} + // bucketPathFor 根据 endpoint 风格决定桶路径(用于建桶 PUT)。 // // 1) 区域端点(推荐): cos.ap-hongkong.myqcloud.com → 路径式 /