package services import ( "encoding/json" "fmt" "path" "strings" ) type sidecarLogMount struct { containerName string volumeName string mountPath string subPath string createVolume bool mountApplication bool } type sidecarDeployment struct { Spec struct { Template struct { Spec struct { Containers []struct { Name string `json:"name"` VolumeMounts []struct { Name string `json:"name"` MountPath string `json:"mountPath"` SubPath string `json:"subPath"` SubPathExpr string `json:"subPathExpr"` ReadOnly bool `json:"readOnly"` } `json:"volumeMounts"` } `json:"containers"` Volumes []struct { Name string `json:"name"` } `json:"volumes"` } `json:"spec"` } `json:"template"` } `json:"spec"` } // resolveSidecarLogMount 优先复用业务容器已有的卷;否则仅在专用日志目录创建共享 emptyDir。 func resolveSidecarLogMount(deploymentJSON []byte, target, logPath string, collectorID uint) (sidecarLogMount, error) { var deployment sidecarDeployment if err := json.Unmarshal(deploymentJSON, &deployment); err != nil { return sidecarLogMount{}, fmt.Errorf("读取目标 Deployment 失败: %w", err) } logDir := path.Dir(strings.TrimSpace(logPath)) if !path.IsAbs(logPath) || logDir == "/" || strings.ContainsAny(logDir, "*?[]\n\r") { return sidecarLogMount{}, fmt.Errorf("Sidecar 采集路径需为专用目录下的绝对文件路径或 glob,例如 /var/log/app/*.log") } containers := deployment.Spec.Template.Spec.Containers var appIndex = -1 var candidate = -1 for i, container := range containers { if container.Name == target { appIndex = i break } if container.Name != "fluent-bit" { if candidate >= 0 { candidate = -2 } else if candidate == -1 { candidate = i } } } if appIndex < 0 { appIndex = candidate } if appIndex < 0 { return sidecarLogMount{}, fmt.Errorf("目标 Deployment 中无法确定业务容器,请使容器名与 Deployment 名一致,或仅保留一个业务容器") } app := containers[appIndex] mount := sidecarLogMount{containerName: app.Name} longest := -1 for _, item := range app.VolumeMounts { prefix := strings.TrimSuffix(item.MountPath, "/") if (logDir == prefix || strings.HasPrefix(logDir, prefix+"/")) && len(prefix) > longest { if item.ReadOnly || item.SubPathExpr != "" { return sidecarLogMount{}, fmt.Errorf("业务容器的日志卷必须可写,且不能使用 subPathExpr") } mount.volumeName = item.Name mount.mountPath = item.MountPath mount.subPath = item.SubPath longest = len(prefix) } } if mount.volumeName != "" { if mount.mountPath == "/tmp" { return sidecarLogMount{}, fmt.Errorf("日志卷不能挂载在 /tmp:Fluent Bit 用此目录保存读取位置;请改用专用日志目录") } return mount, nil } // 自动挂载会遮蔽目录原有内容,仅对明确的专用日志目录执行。 if logDir != "/var/log/app" && path.Base(logDir) != "logs" && path.Base(logDir) != "log" { return sidecarLogMount{}, fmt.Errorf("业务容器未共享 %s;请将采集路径改为 /var/log/app/*.log 或已有日志卷中的文件", logDir) } volumeName := fmt.Sprintf("loki-collector-logs-%d", collectorID) for _, volume := range deployment.Spec.Template.Spec.Volumes { if volume.Name == volumeName { return sidecarLogMount{}, fmt.Errorf("卷 %s 已存在但业务容器未挂载,请先检查目标 Deployment", volumeName) } } mount.volumeName = volumeName mount.mountPath = logDir mount.createVolume = true mount.mountApplication = true return mount, nil } // sidecarRemovePatchYAML 只清理该采集策略注入的卷,保留业务原有的日志卷。 func sidecarRemovePatchYAML(deploymentJSON []byte, collectorID uint) (string, error) { var deployment sidecarDeployment if err := json.Unmarshal(deploymentJSON, &deployment); err != nil { return "", fmt.Errorf("读取目标 Deployment 失败: %w", err) } injectedName := fmt.Sprintf("loki-collector-logs-%d", collectorID) appPatch := "" for _, container := range deployment.Spec.Template.Spec.Containers { if container.Name == "fluent-bit" { continue } for _, mount := range container.VolumeMounts { if mount.Name == injectedName { appPatch += fmt.Sprintf(" - name: %s\n volumeMounts:\n - mountPath: %q\n $patch: delete\n", container.Name, mount.MountPath) } } } volumePatch := "" for _, volume := range deployment.Spec.Template.Spec.Volumes { if volume.Name == injectedName { volumePatch = fmt.Sprintf(" - name: %s\n $patch: delete\n", injectedName) break } } return fmt.Sprintf(`spec: template: spec: containers: - name: fluent-bit $patch: delete %s volumes: - name: fluent-bit-config $patch: delete - name: fluent-bit-state $patch: delete %s`, appPatch, volumePatch), nil }