package services import ( "context" "encoding/base64" "io" "regexp" "strings" "testing" "gopkg.in/yaml.v3" "lokistack/models" ) func TestRedactLokiYAML(t *testing.T) { source := "storage_config:\n aws:\n bucketnames: loki\n access_key_id: ENC:private-access\n secret_access_key: private-secret\nlimits_config:\n retention_period: 720h\n" got, err := redactLokiYAML(source) if err != nil { t.Fatal(err) } for _, secret := range []string{"private-access", "private-secret", "ENC:"} { if strings.Contains(got, secret) { t.Fatalf("sensitive value remains in config: %s", secret) } } for _, want := range []string{"bucketnames: loki", "retention_period: 720h", "access_key_id: '********'"} { if !strings.Contains(got, want) { t.Fatalf("missing %q in sanitized config: %s", want, got) } } } func TestMinIOConfigUsesBucketNameNotPathPrefix(t *testing.T) { d := NewLokiDeployer(&models.LokiDeployment{StorageConfig: &models.StorageConfig{ StorageType: "minio", Bucket: "logs-bucket", PathPrefix: "wrong-prefix", Endpoint: "http://minio:9000", AccessKey: "key", SecretKey: "secret", Insecure: true, }}, nil) config := d.generateLokiConfig() if count := strings.Count(config, "bucketnames: logs-bucket"); count != 2 { t.Fatalf("bucket name should appear in storage and ruler config; got %d", count) } if strings.Contains(config, "wrong-prefix") || strings.Contains(config, "s3://") { t.Fatalf("invalid S3 URL or path prefix in generated config") } } func TestDeployRejectsUndecryptedStorageCredential(t *testing.T) { d := NewLokiDeployer(&models.LokiDeployment{StorageConfig: &models.StorageConfig{ StorageType: "minio", AccessKey: "ENC:unavailable", SecretKey: "secret", }}, nil) if err := d.Deploy(context.Background()); err == nil || !strings.Contains(err.Error(), "存储凭证解密失败") { t.Fatalf("expected credential failure, got %v", err) } } func TestGeneratedLokiConfigKeepsRequiredSettingsWithoutDefaults(t *testing.T) { d := NewLokiDeployer(&models.LokiDeployment{RetentionDays: 90, Replicas: 1, StorageConfig: &models.StorageConfig{ StorageType: "minio", Bucket: "logs", Endpoint: "http://minio:9000", AccessKey: "key", SecretKey: "secret", Insecure: true, }}, nil) config := d.generateLokiConfig() var parsed map[string]interface{} if err := yaml.Unmarshal([]byte(config), &parsed); err != nil { t.Fatalf("invalid Loki YAML: %v", err) } // 注意 delete_request_store 用的是 filesystem 而不是 s3: // loki_config.go 里对 s3/oss/minio/cos/obs 刻意把 compactor 的删除请求存到 // 本地盘(见该处注释),避免各云 S3 兼容实现差异导致 compactor 启动失败。 // loki_object_store_test.go 断言的也是 filesystem —— 两处必须一致。 for _, required := range []string{"retention_period: 2160h", "retention_enabled: true", "delete_request_store: filesystem", "bucketnames: logs", "store: tsdb", "schema: v13"} { if !strings.Contains(config, required) { t.Fatalf("required setting %q missing", required) } } for _, defaultSetting := range []string{"http_listen_port:", "grpc_listen_port:", "chunk_idle_period:", "compaction_interval:", "retention_delete_delay:", "max_entries_limit_per_query:", "frontend_worker:", "chunk_store_config:", "autoforget_unhealthy:"} { if strings.Contains(config, defaultSetting) { t.Fatalf("unnecessary setting %q remained", defaultSetting) } } d.deployment.Replicas = 2 if !strings.Contains(d.generateLokiConfig(), "autoforget_unhealthy: true") { t.Fatal("multi-replica ring recovery setting missing") } } func TestK8sManifestKeepsStorageCredentialsInSecret(t *testing.T) { d := NewLokiDeployer(&models.LokiDeployment{Mode: "k8s-yaml", RetentionDays: 60, Replicas: 1, StorageConfig: &models.StorageConfig{ StorageType: "minio", Bucket: "loki", Endpoint: "http://minio:9000", AccessKey: "test-access", SecretKey: "test-secret", Insecure: true, }}, nil) manifest := d.generateManifest("log") decoder := yaml.NewDecoder(strings.NewReader(manifest)) var configYAML string var secret map[string]interface{} for { var doc map[string]interface{} if err := decoder.Decode(&doc); err != nil { if err == io.EOF { break } t.Fatalf("invalid manifest YAML: %v", err) } switch doc["kind"] { case "ConfigMap": configYAML = doc["data"].(map[string]interface{})["loki.yaml"].(string) case "Secret": secret = doc["data"].(map[string]interface{}) } } if configYAML == "" || secret == nil { t.Fatal("ConfigMap or Secret missing") } if strings.Contains(configYAML, "test-access") || strings.Contains(configYAML, "test-secret") { t.Fatal("storage credential leaked into ConfigMap") } for _, marker := range []string{"${LOKI_STORAGE_ACCESS_KEY}", "${LOKI_STORAGE_SECRET_KEY}", "retention_period: 1440h", "retention_enabled: true"} { if !strings.Contains(configYAML, marker) { t.Fatalf("required Loki setting %q missing", marker) } } if !strings.Contains(manifest, "-config.expand-env=true") || !strings.Contains(manifest, "secretKeyRef:") { t.Fatal("StatefulSet does not reference storage Secret") } for key, want := range map[string]string{"accessKey": "test-access", "secretKey": "test-secret"} { decoded, err := base64.StdEncoding.DecodeString(secret[key].(string)) if err != nil || string(decoded) != want { t.Fatalf("Secret %s data mismatch", key) } } } func TestK8sManifestChangesPodTemplateWhenRetentionChanges(t *testing.T) { deployment := &models.LokiDeployment{RetentionDays: 30, Replicas: 1} d := NewLokiDeployer(deployment, nil) checksum := regexp.MustCompile(`checksum/loki-config: "[a-f0-9]{64}"`) first := checksum.FindString(d.generateManifest("log")) deployment.RetentionDays = 70 second := checksum.FindString(d.generateManifest("log")) if first == "" || second == "" || first == second { t.Fatalf("Pod config checksum must change with retention: %q -> %q", first, second) } }