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 }