Files
LokiStack/backend/services/ingress_access.go
T
opsx f3733f0bfc feat: 品牌统一 LokiStack + 全站中英 i18n + 用户批量删除 + 主题定时切换与浅色侧栏
- 品牌: Go 模块/数据文件/二进制/构建部署资产全部 opsx→lokistack;前端 localStorage 命名空间迁移(读旧写新,含按用户 id 前缀迁移);修复 nginx upstream 与 Makefile 引用已改名服务的断裂
- i18n: react-i18next 落地,common + 11 个页面域命名空间全站中英(菜单/顶栏/登录/列表页/表单/确认弹窗/状态徽章);设置页语言即时切换;修复词典自注册导致的循环导入白屏
- 用户管理: 批量删除(POST /users/batch-delete,不删自己/最后管理员,部分失败不阻断)+ 行复选/全选/批量工具条
- 主题: 浅色侧栏(中性灰激活态)、四态模式(浅色/深色/跟随系统/定时切换,按时区判定)、顶栏太阳月亮二态、设置页紧凑分段选择器
- UI: 登录记住用户名、CardActionButton 统一五页卡片操作钮、shadcn Badge 替换手写徽章、语义色修正
- 清理: 移除主机 Web 终端(前后端)、k8s-helm/Helm 遗留代码与字段、README 重写精简
- 修复: 侧栏与日志告警 order 重复、路由标题 i18n key 未翻译、词典双逗号等
2026-10-27 17:11:38 +08:00

185 lines
7.4 KiB
Go

package services
import (
"context"
"fmt"
"strings"
"time"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"lokistack/models"
)
const ingressNamespace = "ingress-nginx"
const ingressControllerName = "ingress-nginx-controller"
// The field names are shared with the JSON parser on the Loki query page.
const ingressAccessFormat = `{"time":"$time_iso8601","remote_addr":"$remote_addr","host":"$host","http_host":"$http_host","request":"$request","method":"$request_method","uri":"$request_uri","status":$status,"body_bytes_sent":$body_bytes_sent,"http_referer":"$http_referer","http_user_agent":"$http_user_agent","request_length":$request_length,"request_time":$request_time,"proxy_upstream_name":"$proxy_upstream_name","upstream_addr":"$upstream_addr","upstream_status":"$upstream_status","upstream_response_time":"$upstream_response_time"}`
type IngressAccessStatus struct {
Namespace string `json:"namespace"`
Deployment string `json:"deployment"`
Image string `json:"image"`
ConfigMap string `json:"configMap"`
ConfigMapAttached bool `json:"configMapAttached"`
CurrentFormat string `json:"currentFormat"`
TargetFormat string `json:"targetFormat"`
Standardized bool `json:"standardized"`
ManagedBy string `json:"managedBy"`
}
func ingressConfigArg(args []string) string {
for i, arg := range args {
if strings.HasPrefix(arg, "--configmap=") {
return strings.TrimPrefix(arg, "--configmap=")
}
if arg == "--configmap" && i+1 < len(args) {
return args[i+1]
}
}
return ""
}
func ingressAccessState(ctx context.Context, cluster *models.Cluster) (IngressAccessStatus, error) {
client, err := clusterClient(cluster, 10*time.Second)
if err != nil {
return IngressAccessStatus{}, err
}
dep, err := client.AppsV1().Deployments(ingressNamespace).Get(ctx, ingressControllerName, metav1.GetOptions{})
if err != nil {
return IngressAccessStatus{}, fmt.Errorf("读取 ingress-nginx 控制器失败: %w", err)
}
index := -1
for i, container := range dep.Spec.Template.Spec.Containers {
if strings.Contains(container.Image, "ingress-nginx/controller:") {
index = i
break
}
}
if index < 0 {
return IngressAccessStatus{}, fmt.Errorf("未识别到社区 ingress-nginx controller 容器")
}
container := dep.Spec.Template.Spec.Containers[index]
configName := ingressConfigArg(container.Args)
if configName == "" {
configName = ingressNamespace + "/" + ingressControllerName
}
parts := strings.Split(configName, "/")
if len(parts) != 2 || parts[0] != ingressNamespace || parts[1] == "" {
return IngressAccessStatus{}, fmt.Errorf("控制器使用其他命名空间的 ConfigMap: %s", configName)
}
cm, err := client.CoreV1().ConfigMaps(parts[0]).Get(ctx, parts[1], metav1.GetOptions{})
if err != nil {
return IngressAccessStatus{}, fmt.Errorf("读取 ingress-nginx ConfigMap 失败: %w", err)
}
format := cm.Data["log-format-upstream"]
return IngressAccessStatus{
Namespace: ingressNamespace, Deployment: dep.Name, Image: container.Image,
ConfigMap: configName, ConfigMapAttached: ingressConfigArg(container.Args) != "",
CurrentFormat: format, TargetFormat: ingressAccessFormat,
Standardized: format == ingressAccessFormat && cm.Data["log-format-escape-json"] == "true" && ingressConfigArg(container.Args) != "",
ManagedBy: dep.Labels["app.kubernetes.io/managed-by"],
}, nil
}
func InspectIngressAccess(ctx context.Context, cluster *models.Cluster) (IngressAccessStatus, error) {
return ingressAccessState(ctx, cluster)
}
// ApplyIngressAccess changes only the two log keys, preserving unrelated ingress settings.
// Explicitly attaching the ConfigMap is necessary for controllers launched without --configmap.
func ApplyIngressAccess(ctx context.Context, cluster *models.Cluster) (IngressAccessStatus, error) {
state, err := ingressAccessState(ctx, cluster)
if err != nil {
return state, err
}
client, err := clusterClient(cluster, 10*time.Second)
if err != nil {
return state, err
}
parts := strings.Split(state.ConfigMap, "/")
cm, err := client.CoreV1().ConfigMaps(parts[0]).Get(ctx, parts[1], metav1.GetOptions{})
if err != nil {
return state, err
}
if !state.ConfigMapAttached {
for key := range cm.Data {
if key != "log-format-upstream" && key != "log-format-escape-json" {
return state, fmt.Errorf("控制器尚未关联 ConfigMap,且其中已有 %s 等设置;为避免意外启用原有配置,已跳过自动配置", key)
}
}
}
previousFormat := strings.Replace(ingressAccessFormat, `"request":"$request",`, "", 1)
if current := cm.Data["log-format-upstream"]; current != "" && current != ingressAccessFormat && current != previousFormat {
return state, fmt.Errorf("已有自定义 access log 格式;为避免覆盖,已跳过自动配置")
}
if state.Standardized {
if err := ensureIngressRolloutStrategy(ctx, cluster); err != nil {
return state, err
}
return state, nil
}
if cm.Data == nil {
cm.Data = make(map[string]string)
}
cm.Data["log-format-upstream"] = ingressAccessFormat
cm.Data["log-format-escape-json"] = "true"
if _, err = client.CoreV1().ConfigMaps(parts[0]).Update(ctx, cm, metav1.UpdateOptions{}); err != nil {
return state, fmt.Errorf("更新 ingress-nginx ConfigMap 失败: %w", err)
}
if !state.ConfigMapAttached {
dep, err := client.AppsV1().Deployments(ingressNamespace).Get(ctx, ingressControllerName, metav1.GetOptions{})
if err != nil {
return state, err
}
for i := range dep.Spec.Template.Spec.Containers {
container := &dep.Spec.Template.Spec.Containers[i]
if strings.Contains(container.Image, "ingress-nginx/controller:") {
container.Args = append(container.Args, "--configmap="+state.ConfigMap)
break
}
}
setIngressRolloutStrategy(dep)
if _, err = client.AppsV1().Deployments(ingressNamespace).Update(ctx, dep, metav1.UpdateOptions{}); err != nil {
return state, fmt.Errorf("ConfigMap 已更新,但控制器尚未关联配置;更新 Deployment 失败: %w", err)
}
}
return ingressAccessState(ctx, cluster)
}
// Host-network replicas occupying every ingress node cannot use a surge pod:
// the new pod would need the same 80/443 ports as an existing replica.
func setIngressRolloutStrategy(dep *appsv1.Deployment) bool {
if !dep.Spec.Template.Spec.HostNetwork || dep.Spec.Replicas == nil || *dep.Spec.Replicas < 2 {
return false
}
zero, one := intstr.FromInt32(0), intstr.FromInt32(1)
strategy := &dep.Spec.Strategy
if strategy.Type == appsv1.RecreateDeploymentStrategyType {
return false
}
if strategy.RollingUpdate != nil && strategy.RollingUpdate.MaxSurge != nil && *strategy.RollingUpdate.MaxSurge == zero && strategy.RollingUpdate.MaxUnavailable != nil && *strategy.RollingUpdate.MaxUnavailable == one {
return false
}
strategy.Type = appsv1.RollingUpdateDeploymentStrategyType
strategy.RollingUpdate = &appsv1.RollingUpdateDeployment{MaxSurge: &zero, MaxUnavailable: &one}
return true
}
func ensureIngressRolloutStrategy(ctx context.Context, cluster *models.Cluster) error {
client, err := clusterClient(cluster, 10*time.Second)
if err != nil {
return err
}
dep, err := client.AppsV1().Deployments(ingressNamespace).Get(ctx, ingressControllerName, metav1.GetOptions{})
if err != nil {
return err
}
if setIngressRolloutStrategy(dep) {
_, err = client.AppsV1().Deployments(ingressNamespace).Update(ctx, dep, metav1.UpdateOptions{})
}
return err
}