feat: 路由改名/kubectl预装/导出YAML精简/Service标签页优化/Alpine镜像源换清华

- 路由 /deploy/credentials → /deploy/configureTools
- kubectl 改为预装(Dockerfile) + 种子数据 preinstall
- 导出YAML精简: Service/Ingress/Deployment 全面清理运行时字段
- DeploymentEditService 只显示匹配 selector 的关联 Service
- Alpine 镜像源 mirrors.aliyun.com → mirrors.tuna.tsinghua.edu.cn
This commit is contained in:
attacker committed 2026-06-23 02:22:52 +00:00
1 parent dd7cb6895e
commit 451f65f9c8
9 files changed
+109 -29

No files matched your search

+8 -4
View File
@@ -35,7 +35,7 @@ ARG BUILD_TIME
WORKDIR /app WORKDIR /app
# 使用国内 Alpine 镜像源加速 # 使用国内 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 RUN apk add --no-cache git ca-certificates tzdata
ENV TZ=Asia/Shanghai 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 FROM alpine:latest
# 使用国内 Alpine 镜像源加速 # 使用国内 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 # 安装运行时依赖(核心最小集,重型工具改为懒加载)// Logan.Li
# bash=kubectl交互终端, curl=健康检查, git=流水线代码拉取, docker-cli=镜像构建推送 # bash=kubectl交互终端, curl=健康检查, git=流水线代码拉取, docker-cli=镜像构建推送
# kubectl/maven/nodejs/helm → 见「发布配置→工具配置」Tab,首次使用自动下载 # kubectl=预装(~50MB),maven/nodejs/helm → 工具配置Tab懒加载
# Pod文件浏览/exec 通过 WebSocket channel 协议直连 K8s API,无需 kubectl
RUN apk --no-cache add ca-certificates tzdata bash curl git docker-cli docker-cli-buildx 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 懒加载后生效) # Maven 阿里云镜像加速(maven 懒加载后生效)
RUN mkdir -p /root/.m2 && \ RUN mkdir -p /root/.m2 && \
echo '<?xml version="1.0" encoding="UTF-8"?><settings><mirrors><mirror><id>aliyun</id><mirrorOf>central</mirrorOf><name>Aliyun Maven</name><url>https://maven.aliyun.com/repository/public</url></mirror></mirrors></settings>' > /root/.m2/settings.xml echo '<?xml version="1.0" encoding="UTF-8"?><settings><mirrors><mirror><id>aliyun</id><mirrorOf>central</mirrorOf><name>Aliyun Maven</name><url>https://maven.aliyun.com/repository/public</url></mirror></mirrors></settings>' > /root/.m2/settings.xml
+8 -3
View File
@@ -17,7 +17,7 @@ FROM golang:1.24-alpine AS builder
WORKDIR /app WORKDIR /app
# 使用国内 Alpine 镜像源加速 // Logan.Li # 使用国内 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 RUN apk add --no-cache git ca-certificates tzdata
ENV TZ=Asia/Shanghai ENV TZ=Asia/Shanghai
@@ -74,15 +74,20 @@ RUN --mount=type=cache,target=/go/pkg/mod \
FROM alpine:3.20 FROM alpine:3.20
# 使用国内 Alpine 镜像源加速 // Logan.Li # 使用国内 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 # 安装运行时依赖(核心最小集,重型工具改为懒加载)// Logan.Li
# bash=kubectl交互终端, curl=健康检查, git=流水线代码拉取, docker-cli=镜像构建推送 # 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 # Pod文件浏览/exec 通过 WebSocket channel 协议直连 K8s API,无需 kubectl
RUN --mount=type=cache,target=/var/cache/apk \ RUN --mount=type=cache,target=/var/cache/apk \
apk add ca-certificates tzdata bash curl git docker-cli docker-cli-buildx 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 懒加载后生效) # npm 淘宝镜像加速(node 懒加载后生效)
RUN npm config set registry https://registry.npmmirror.com 2>/dev/null || true RUN npm config set registry https://registry.npmmirror.com 2>/dev/null || true
# Maven 阿里云镜像加速(maven 懒加载后生效) # Maven 阿里云镜像加速(maven 懒加载后生效)
+66 -5
View File
@@ -269,6 +269,10 @@ func cleanResourceYAML(obj map[string]interface{}) {
delete(meta, "annotations") delete(meta, "annotations")
} }
} }
// 移除空的 labels
if labels, ok := meta["labels"].(map[string]interface{}); ok && len(labels) == 0 {
delete(meta, "labels")
}
} }
// 删除 status // 删除 status
@@ -276,11 +280,21 @@ func cleanResourceYAML(obj map[string]interface{}) {
// 清理 spec 中的运行时字段 // 清理 spec 中的运行时字段
if spec, ok := obj["spec"].(map[string]interface{}); ok { if spec, ok := obj["spec"].(map[string]interface{}); ok {
// Service: 移除动态分配的 clusterIP 和 nodePorts kind, _ := obj["kind"].(string)
if obj["kind"] == "Service" {
// Service: 移除动态分配字段和默认值
if kind == "Service" {
delete(spec, "clusterIP") delete(spec, "clusterIP")
delete(spec, "clusterIPs") 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 { if ports, ok := spec["ports"].([]interface{}); ok {
for _, p := range ports { for _, p := range ports {
if pm, ok := p.(map[string]interface{}); ok { if pm, ok := p.(map[string]interface{}); ok {
@@ -290,8 +304,9 @@ func cleanResourceYAML(obj map[string]interface{}) {
} }
} }
// Deployment: 清理 template.metadata 和 template.spec 中的运行时字段 // Deployment / StatefulSet / DaemonSet: 清理运行时和默认值
if obj["kind"] == "Deployment" { if kind == "Deployment" || kind == "StatefulSet" || kind == "DaemonSet" {
// 清理 template
if template, ok := spec["template"].(map[string]interface{}); ok { if template, ok := spec["template"].(map[string]interface{}); ok {
if tMeta, ok := template["metadata"].(map[string]interface{}); ok { if tMeta, ok := template["metadata"].(map[string]interface{}); ok {
delete(tMeta, "creationTimestamp") delete(tMeta, "creationTimestamp")
@@ -304,8 +319,54 @@ func cleanResourceYAML(obj map[string]interface{}) {
} }
if tSpec, ok := template["spec"].(map[string]interface{}); ok { if tSpec, ok := template["spec"].(map[string]interface{}); ok {
delete(tSpec, "nodeName") 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")
}
} }
} }
} }
+9 -7
View File
@@ -1605,13 +1605,14 @@ func (h *KubernetesHandler) listDeploymentsWithNS(c *gin.Context, ns string) {
deps, err := client.ListDeployments() deps, err := client.ListDeployments()
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
type DepSummary struct { type DepSummary struct {
Namespace string `json:"namespace"` Namespace string `json:"namespace"`
Name string `json:"name"` Name string `json:"name"`
Replicas int `json:"replicas"` Replicas int `json:"replicas"`
AvailableReplicas int `json:"available_replicas"` AvailableReplicas int `json:"available_replicas"`
ReadyReplicas int `json:"ready_replicas"` ReadyReplicas int `json:"ready_replicas"`
Image string `json:"image"` Image string `json:"image"`
Paused bool `json:"paused"` Paused bool `json:"paused"`
Selector map[string]string `json:"selector,omitempty"` // matchLabels — 前端 Service Tab 关联用
} }
result := []DepSummary{} result := []DepSummary{}
for _, d := range deps.Items { for _, d := range deps.Items {
@@ -1625,6 +1626,7 @@ func (h *KubernetesHandler) listDeploymentsWithNS(c *gin.Context, ns string) {
ReadyReplicas: d.Status.ReadyReplicas, ReadyReplicas: d.Status.ReadyReplicas,
Image: image, Image: image,
Paused: d.Spec.Paused, 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}) c.JSON(200, models.Response{Code: 200, Data: result})
+2 -2
View File
@@ -66,9 +66,9 @@ func PredefinedTools() []DeployTool {
{ {
Name: "kubectl", Name: "kubectl",
DisplayName: "Kubectl", DisplayName: "Kubectl",
Description: "Kubernetes 命令行工具,用于 K8s 集群交互", Description: "Kubernetes 命令行工具,用于 K8s 集群交互(已预装在镜像中)",
Category: "k8s", Category: "k8s",
InstallMethod: ToolLazy, InstallMethod: ToolPreinstall,
BinaryName: "kubectl", BinaryName: "kubectl",
InstallPath: "/usr/local/bin/kubectl", InstallPath: "/usr/local/bin/kubectl",
CheckCommand: "kubectl version --client --short 2>/dev/null || kubectl version --client 2>/dev/null", CheckCommand: "kubectl version --client --short 2>/dev/null || kubectl version --client 2>/dev/null",
+6
View File
@@ -290,10 +290,16 @@ type DeploymentItem struct {
type DeploymentSpec struct { type DeploymentSpec struct {
Replicas int `json:"replicas"` Replicas int `json:"replicas"`
Selector *LabelSelector `json:"selector,omitempty"` // matchLabels — 前端 Service 关联用
Template DeploymentTemplate `json:"template"` Template DeploymentTemplate `json:"template"`
Paused bool `json:"paused"` Paused bool `json:"paused"`
} }
// LabelSelector K8s 标签选择器
type LabelSelector struct {
MatchLabels map[string]string `json:"matchLabels,omitempty"`
}
type DeploymentTemplate struct { type DeploymentTemplate struct {
Spec PodSpec `json:"spec"` Spec PodSpec `json:"spec"`
} }
+1 -1
View File
@@ -147,7 +147,7 @@ function App() {
<Route path="pipelines/:code" element={<PipelineEditor />} /> <Route path="pipelines/:code" element={<PipelineEditor />} />
<Route path="pipelines/:code/runs" element={<PipelineRunDetail />} /> <Route path="pipelines/:code/runs" element={<PipelineRunDetail />} />
<Route path="pipelines/:code/edit" element={<PipelineEditor />} /> <Route path="pipelines/:code/edit" element={<PipelineEditor />} />
<Route path="deploy/credentials" element={<DeployCredentials />} /> <Route path="deploy/configureTools" element={<DeployCredentials />} />
<Route path="deploy/groups" element={<UserGroupsManager />} /> <Route path="deploy/groups" element={<UserGroupsManager />} />
<Route path="deploy/rbac" element={<DeployRBAC />} /> <Route path="deploy/rbac" element={<DeployRBAC />} />
{/* 监控告警 */} {/* 监控告警 */}
@@ -67,8 +67,10 @@ export function DeploymentEditService({ clusterCode, namespace, deploymentName,
const load = useCallback(async () => { const load = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
// 先从 manifest 取 selector // 先从 manifest 取 selector(兼容两种来源)
const sel = manifest?.spec?.selector?.matchLabels || {}; // full K8s manifest: spec.selector.matchLabels
// DepSummary 降级: selector (顶层)
const sel = manifest?.spec?.selector?.matchLabels || manifest?.selector || {};
setSelectorLabels(sel); setSelectorLabels(sel);
const [svcRes, ingRes] = await Promise.all([ const [svcRes, ingRes] = await Promise.all([
@@ -275,7 +277,7 @@ export function DeploymentEditService({ clusterCode, namespace, deploymentName,
<CardTitle className="text-sm text-slate-200 flex items-center gap-2"> <CardTitle className="text-sm text-slate-200 flex items-center gap-2">
<Network className="h-4 w-4 text-blue-400" /> <Network className="h-4 w-4 text-blue-400" />
Services Services
<Badge variant="outline" className="border-slate-700 text-slate-400">{matchedSvcs.length}/{services.length}</Badge> <Badge variant="outline" className="border-slate-700 text-slate-400">{matchedSvcs.length} 个关联</Badge>
</CardTitle> </CardTitle>
<Button size="sm" variant="outline" className="h-7 border-slate-700 text-slate-300 text-xs" <Button size="sm" variant="outline" className="h-7 border-slate-700 text-slate-300 text-xs"
onClick={() => { setSvcForm(s => ({ ...s, name: `${deploymentName}-svc`, port: '80', targetPort: '80' })); setShowCreate('service'); }}> onClick={() => { setSvcForm(s => ({ ...s, name: `${deploymentName}-svc`, port: '80', targetPort: '80' })); setShowCreate('service'); }}>
@@ -283,18 +285,18 @@ export function DeploymentEditService({ clusterCode, namespace, deploymentName,
</Button> </Button>
</div> </div>
<p className="text-xs text-slate-500 mt-1"> <p className="text-xs text-slate-500 mt-1">
高亮的是 selector 匹配此 Deployment 的 Service({matchedSvcs.length} 个关联 / {services.length} 个总数) 仅显示 selector 匹配此 Deployment 的关联 Service
</p> </p>
</CardHeader> </CardHeader>
<CardContent className="pt-0"> <CardContent className="pt-0">
{services.length === 0 ? ( {matchedSvcs.length === 0 ? (
<div className="text-center py-6 text-slate-500 text-sm border border-dashed border-slate-800 rounded-lg"> <div className="text-center py-6 text-slate-500 text-sm border border-dashed border-slate-800 rounded-lg">
<Network className="h-8 w-8 mx-auto mb-2 text-slate-700" /> <Network className="h-8 w-8 mx-auto mb-2 text-slate-700" />
当前命名空间暂无 Service 当前命名空间暂无 Service
</div> </div>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{services.map(svc => { {matchedSvcs.map(svc => {
const matched = selectorMatches(svc.selector); const matched = selectorMatches(svc.selector);
return ( return (
<div key={svc.name} className={`rounded-lg border p-3 ${matched ? 'border-blue-700/50 bg-blue-950/20' : 'border-slate-800 bg-slate-950/30'}`}> <div key={svc.name} className={`rounded-lg border p-3 ${matched ? 'border-blue-700/50 bg-blue-950/20' : 'border-slate-800 bg-slate-950/30'}`}>
+1 -1
View File
@@ -74,7 +74,7 @@ const menuItems: MenuItem[] = [
id: 'deploy', name: '应用发布', icon: Rocket, id: 'deploy', name: '应用发布', icon: Rocket,
children: [ children: [
{ id: 'pipelines', name: '流水线', icon: GitBranch, path: '/pipelines' }, { 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' }, { id: 'deploy-rbac', name: '发布权限', icon: Shield, path: '/deploy/rbac' },
], ],
}, },