diff --git a/Dockerfile.embed b/Dockerfile.embed
index e1bbb57..e2fb96c 100644
--- a/Dockerfile.embed
+++ b/Dockerfile.embed
@@ -35,7 +35,7 @@ ARG BUILD_TIME
WORKDIR /app
# 使用国内 Alpine 镜像源加速
-RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories
+RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apk/repositories
RUN apk add --no-cache git ca-certificates tzdata
ENV TZ=Asia/Shanghai
@@ -82,14 +82,18 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.vers
FROM alpine:latest
# 使用国内 Alpine 镜像源加速
-RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories
+RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apk/repositories
# 安装运行时依赖(核心最小集,重型工具改为懒加载)// Logan.Li
# bash=kubectl交互终端, curl=健康检查, git=流水线代码拉取, docker-cli=镜像构建推送
-# kubectl/maven/nodejs/helm → 见「发布配置→工具配置」Tab,首次使用自动下载
-# Pod文件浏览/exec 通过 WebSocket channel 协议直连 K8s API,无需 kubectl
+# kubectl=预装(~50MB),maven/nodejs/helm → 工具配置Tab懒加载
RUN apk --no-cache add ca-certificates tzdata bash curl git docker-cli docker-cli-buildx
+# kubectl 预装 // Logan.Li
+ARG KUBECTL_VERSION=v1.32.0
+RUN curl -sLo /usr/local/bin/kubectl https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl && \
+ chmod +x /usr/local/bin/kubectl
+
# Maven 阿里云镜像加速(maven 懒加载后生效)
RUN mkdir -p /root/.m2 && \
echo 'aliyuncentralAliyun Mavenhttps://maven.aliyun.com/repository/public' > /root/.m2/settings.xml
diff --git a/backend/Dockerfile b/backend/Dockerfile
index b677f3b..e727e01 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -17,7 +17,7 @@ FROM golang:1.24-alpine AS builder
WORKDIR /app
# 使用国内 Alpine 镜像源加速 // Logan.Li
-RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories
+RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apk/repositories
RUN apk add --no-cache git ca-certificates tzdata
ENV TZ=Asia/Shanghai
@@ -74,15 +74,20 @@ RUN --mount=type=cache,target=/go/pkg/mod \
FROM alpine:3.20
# 使用国内 Alpine 镜像源加速 // Logan.Li
-RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories
+RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apk/repositories
# 安装运行时依赖(核心最小集,重型工具改为懒加载)// Logan.Li
# bash=kubectl交互终端, curl=健康检查, git=流水线代码拉取, docker-cli=镜像构建推送
-# kubectl/maven/nodejs/helm → 见「发布配置→工具配置」Tab,首次使用自动下载
+# kubectl=预装(~50MB,K8s交互终端必需),maven/nodejs/helm → 工具配置Tab懒加载
# Pod文件浏览/exec 通过 WebSocket channel 协议直连 K8s API,无需 kubectl
RUN --mount=type=cache,target=/var/cache/apk \
apk add ca-certificates tzdata bash curl git docker-cli docker-cli-buildx
+# kubectl 预装(~50MB,国内网络从 dl.k8s.io 下载约10s)// Logan.Li
+ARG KUBECTL_VERSION=v1.32.0
+RUN curl -sLo /usr/local/bin/kubectl https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl && \
+ chmod +x /usr/local/bin/kubectl
+
# npm 淘宝镜像加速(node 懒加载后生效)
RUN npm config set registry https://registry.npmmirror.com 2>/dev/null || true
# Maven 阿里云镜像加速(maven 懒加载后生效)
diff --git a/backend/internal/api/handlers/k8s_export.go b/backend/internal/api/handlers/k8s_export.go
index 23f134c..13985cc 100644
--- a/backend/internal/api/handlers/k8s_export.go
+++ b/backend/internal/api/handlers/k8s_export.go
@@ -269,6 +269,10 @@ func cleanResourceYAML(obj map[string]interface{}) {
delete(meta, "annotations")
}
}
+ // 移除空的 labels
+ if labels, ok := meta["labels"].(map[string]interface{}); ok && len(labels) == 0 {
+ delete(meta, "labels")
+ }
}
// 删除 status
@@ -276,11 +280,21 @@ func cleanResourceYAML(obj map[string]interface{}) {
// 清理 spec 中的运行时字段
if spec, ok := obj["spec"].(map[string]interface{}); ok {
- // Service: 移除动态分配的 clusterIP 和 nodePorts
- if obj["kind"] == "Service" {
+ kind, _ := obj["kind"].(string)
+
+ // Service: 移除动态分配字段和默认值
+ if kind == "Service" {
delete(spec, "clusterIP")
delete(spec, "clusterIPs")
- // nodePorts 在 ports 数组内 — 仅移除 NodePort 类型的 nodePort 字段
+ delete(spec, "internalTrafficPolicy")
+ delete(spec, "ipFamilies")
+ delete(spec, "ipFamilyPolicy")
+ // sessionAffinity 默认 "None" → 移除
+ if sa, ok := spec["sessionAffinity"].(string); ok && sa == "None" {
+ delete(spec, "sessionAffinity")
+ }
+ delete(spec, "allocateLoadBalancerNodePorts")
+ // 移除 ports 内的 nodePort(仅 NodePort/LoadBalancer 类型)
if ports, ok := spec["ports"].([]interface{}); ok {
for _, p := range ports {
if pm, ok := p.(map[string]interface{}); ok {
@@ -290,8 +304,9 @@ func cleanResourceYAML(obj map[string]interface{}) {
}
}
- // Deployment: 清理 template.metadata 和 template.spec 中的运行时字段
- if obj["kind"] == "Deployment" {
+ // Deployment / StatefulSet / DaemonSet: 清理运行时和默认值
+ if kind == "Deployment" || kind == "StatefulSet" || kind == "DaemonSet" {
+ // 清理 template
if template, ok := spec["template"].(map[string]interface{}); ok {
if tMeta, ok := template["metadata"].(map[string]interface{}); ok {
delete(tMeta, "creationTimestamp")
@@ -304,8 +319,54 @@ func cleanResourceYAML(obj map[string]interface{}) {
}
if tSpec, ok := template["spec"].(map[string]interface{}); ok {
delete(tSpec, "nodeName")
+ delete(tSpec, "serviceAccountName") // 移除 default 值
+ // 清理容器内的 terminationMessagePath/Policy 默认值
+ if containers, ok := tSpec["containers"].([]interface{}); ok {
+ for _, c := range containers {
+ if cm, ok := c.(map[string]interface{}); ok {
+ if tmp, ok := cm["terminationMessagePath"].(string); ok && tmp == "/dev/termination-log" {
+ delete(cm, "terminationMessagePath")
+ }
+ if tmp, ok := cm["terminationMessagePolicy"].(string); ok && tmp == "File" {
+ delete(cm, "terminationMessagePolicy")
+ }
+ // 清理默认 imagePullPolicy
+ if ip, ok := cm["imagePullPolicy"].(string); ok && ip == "Always" {
+ delete(cm, "imagePullPolicy")
+ }
+ // 清理空 resources
+ if res, ok := cm["resources"].(map[string]interface{}); ok && len(res) == 0 {
+ delete(cm, "resources")
+ }
+ }
+ }
+ }
+ // 清理 dnsPolicy 默认值
+ if dp, ok := tSpec["dnsPolicy"].(string); ok && dp == "ClusterFirst" {
+ delete(tSpec, "dnsPolicy")
+ }
+ if rp, ok := tSpec["restartPolicy"].(string); ok && rp == "Always" {
+ delete(tSpec, "restartPolicy")
+ }
+ if tp, ok := tSpec["terminationGracePeriodSeconds"].(float64); ok && tp == 30 {
+ delete(tSpec, "terminationGracePeriodSeconds")
+ }
}
}
+ // 清理默认值
+ if rhl, ok := spec["revisionHistoryLimit"].(float64); ok && rhl == 10 {
+ delete(spec, "revisionHistoryLimit")
+ }
+ if pds, ok := spec["progressDeadlineSeconds"].(float64); ok && pds == 600 {
+ delete(spec, "progressDeadlineSeconds")
+ }
+ }
+
+ // Ingress: 清理默认值
+ if kind == "Ingress" {
+ if ic, ok := spec["ingressClassName"].(string); ok && ic == "" {
+ delete(spec, "ingressClassName")
+ }
}
}
}
diff --git a/backend/internal/api/handlers/kubernetes.go b/backend/internal/api/handlers/kubernetes.go
index b691e64..ca9e52e 100644
--- a/backend/internal/api/handlers/kubernetes.go
+++ b/backend/internal/api/handlers/kubernetes.go
@@ -1605,13 +1605,14 @@ func (h *KubernetesHandler) listDeploymentsWithNS(c *gin.Context, ns string) {
deps, err := client.ListDeployments()
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
type DepSummary struct {
- Namespace string `json:"namespace"`
- Name string `json:"name"`
- Replicas int `json:"replicas"`
- AvailableReplicas int `json:"available_replicas"`
- ReadyReplicas int `json:"ready_replicas"`
- Image string `json:"image"`
- Paused bool `json:"paused"`
+ Namespace string `json:"namespace"`
+ Name string `json:"name"`
+ Replicas int `json:"replicas"`
+ AvailableReplicas int `json:"available_replicas"`
+ ReadyReplicas int `json:"ready_replicas"`
+ Image string `json:"image"`
+ Paused bool `json:"paused"`
+ Selector map[string]string `json:"selector,omitempty"` // matchLabels — 前端 Service Tab 关联用
}
result := []DepSummary{}
for _, d := range deps.Items {
@@ -1625,6 +1626,7 @@ func (h *KubernetesHandler) listDeploymentsWithNS(c *gin.Context, ns string) {
ReadyReplicas: d.Status.ReadyReplicas,
Image: image,
Paused: d.Spec.Paused,
+ Selector: func() map[string]string { if d.Spec.Selector != nil { return d.Spec.Selector.MatchLabels }; return nil }(),
})
}
c.JSON(200, models.Response{Code: 200, Data: result})
diff --git a/backend/internal/models/deploy_tool.go b/backend/internal/models/deploy_tool.go
index 207c7f0..1750e64 100644
--- a/backend/internal/models/deploy_tool.go
+++ b/backend/internal/models/deploy_tool.go
@@ -66,9 +66,9 @@ func PredefinedTools() []DeployTool {
{
Name: "kubectl",
DisplayName: "Kubectl",
- Description: "Kubernetes 命令行工具,用于 K8s 集群交互",
+ Description: "Kubernetes 命令行工具,用于 K8s 集群交互(已预装在镜像中)",
Category: "k8s",
- InstallMethod: ToolLazy,
+ InstallMethod: ToolPreinstall,
BinaryName: "kubectl",
InstallPath: "/usr/local/bin/kubectl",
CheckCommand: "kubectl version --client --short 2>/dev/null || kubectl version --client 2>/dev/null",
diff --git a/backend/pkg/kubernetesclient/client.go b/backend/pkg/kubernetesclient/client.go
index 469ce75..33a438b 100644
--- a/backend/pkg/kubernetesclient/client.go
+++ b/backend/pkg/kubernetesclient/client.go
@@ -290,10 +290,16 @@ type DeploymentItem struct {
type DeploymentSpec struct {
Replicas int `json:"replicas"`
+ Selector *LabelSelector `json:"selector,omitempty"` // matchLabels — 前端 Service 关联用
Template DeploymentTemplate `json:"template"`
Paused bool `json:"paused"`
}
+// LabelSelector K8s 标签选择器
+type LabelSelector struct {
+ MatchLabels map[string]string `json:"matchLabels,omitempty"`
+}
+
type DeploymentTemplate struct {
Spec PodSpec `json:"spec"`
}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 5616811..cc07ef1 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -147,7 +147,7 @@ function App() {
} />
} />
} />
- } />
+ } />
} />
} />
{/* 监控告警 */}
diff --git a/frontend/src/components/kubernetes/DeploymentEditService.tsx b/frontend/src/components/kubernetes/DeploymentEditService.tsx
index 66bf998..93ff5a5 100644
--- a/frontend/src/components/kubernetes/DeploymentEditService.tsx
+++ b/frontend/src/components/kubernetes/DeploymentEditService.tsx
@@ -67,8 +67,10 @@ export function DeploymentEditService({ clusterCode, namespace, deploymentName,
const load = useCallback(async () => {
setLoading(true);
try {
- // 先从 manifest 取 selector
- const sel = manifest?.spec?.selector?.matchLabels || {};
+ // 先从 manifest 取 selector(兼容两种来源)
+ // full K8s manifest: spec.selector.matchLabels
+ // DepSummary 降级: selector (顶层)
+ const sel = manifest?.spec?.selector?.matchLabels || manifest?.selector || {};
setSelectorLabels(sel);
const [svcRes, ingRes] = await Promise.all([
@@ -275,7 +277,7 @@ export function DeploymentEditService({ clusterCode, namespace, deploymentName,
Services
- {matchedSvcs.length}/{services.length}
+ {matchedSvcs.length} 个关联
- 高亮的是 selector 匹配此 Deployment 的 Service({matchedSvcs.length} 个关联 / {services.length} 个总数)
+ 仅显示 selector 匹配此 Deployment 的关联 Service
- {services.length === 0 ? (
+ {matchedSvcs.length === 0 ? (
当前命名空间暂无 Service
) : (
- {services.map(svc => {
+ {matchedSvcs.map(svc => {
const matched = selectorMatches(svc.selector);
return (
diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx
index 67ce71a..cf556e4 100644
--- a/frontend/src/components/layout/Sidebar.tsx
+++ b/frontend/src/components/layout/Sidebar.tsx
@@ -74,7 +74,7 @@ const menuItems: MenuItem[] = [
id: 'deploy', name: '应用发布', icon: Rocket,
children: [
{ id: 'pipelines', name: '流水线', icon: GitBranch, path: '/pipelines' },
- { id: 'deploy-credentials', name: '发布配置', icon: Key, path: '/deploy/credentials' },
+ { id: 'deploy-credentials', name: '发布配置', icon: Key, path: '/deploy/configureTools' },
{ id: 'deploy-rbac', name: '发布权限', icon: Shield, path: '/deploy/rbac' },
],
},