- 品牌: 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 未翻译、词典双逗号等
185 lines
7.4 KiB
Go
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
|
|
}
|