Add the Loki query UI, access policies, ingress insights, alert and registry settings, and their backend APIs. Include the related Kubernetes deployment and collector updates required by the integrated application. Validated with go test ./..., npm run build, and npm run lint.
151 lines
4.9 KiB
Go
151 lines
4.9 KiB
Go
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
|
||
}
|