package toolinstaller import ( "log" "net/http" "strings" "time" ) // ============================================ // 工具镜像源管理 — 每工具 ≥2 国内源 + 官方源 // 下载前先 HEAD 验证 URL 可达,再 fallback 重试 // ============================================ // ToolMirror 单个镜像源定义 type ToolMirror struct { Name string // 镜像名称(日志用) URL string // 下载地址模板,{version} 占位符 // Location 标记:domestic(国内)/global(国际)/internal(内网) Location string } // 工具镜像源清单 // 每个工具至少 2 个国内源 + 官方源 var toolMirrors = map[string][]ToolMirror{ "kubectl": { {Name: "K8s官方", URL: "https://dl.k8s.io/release/{version}/bin/linux/amd64/kubectl", Location: "global"}, {Name: "GCS直链", URL: "https://storage.googleapis.com/kubernetes-release/release/{version}/bin/linux/amd64/kubectl", Location: "global"}, // 国内无 kubectl 直链镜像(阿里云/清华只托管 yum/apt 源),此处预留给内网 Harbor // {Name: "内网Harbor", URL: "https://harbor.ops.net/static/kubectl-{version}-linux-amd64", Location: "internal"}, }, "maven": { {Name: "Apache官方", URL: "https://dlcdn.apache.org/maven/maven-3/{version}/binaries/apache-maven-{version}-bin.tar.gz", Location: "global"}, {Name: "清华镜像", URL: "https://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/{version}/binaries/apache-maven-{version}-bin.tar.gz", Location: "domestic"}, {Name: "阿里云镜像", URL: "https://mirrors.aliyun.com/apache/maven/maven-3/{version}/binaries/apache-maven-{version}-bin.tar.gz", Location: "domestic"}, {Name: "华为云镜像", URL: "https://repo.huaweicloud.com/apache/maven/maven-3/{version}/binaries/apache-maven-{version}-bin.tar.gz", Location: "domestic"}, }, "nodejs": { {Name: "Node官方", URL: "https://nodejs.org/dist/{version}/node-{version}-linux-x64.tar.xz", Location: "global"}, {Name: "npmmirror", URL: "https://npmmirror.com/mirrors/node/{version}/node-{version}-linux-x64.tar.xz", Location: "domestic"}, {Name: "清华镜像", URL: "https://mirrors.tuna.tsinghua.edu.cn/nodejs-release/{version}/node-{version}-linux-x64.tar.xz", Location: "domestic"}, {Name: "华为云镜像", URL: "https://repo.huaweicloud.com/nodejs/{version}/node-{version}-linux-x64.tar.xz", Location: "domestic"}, {Name: "阿里云镜像", URL: "https://mirrors.aliyun.com/nodejs-release/{version}/node-{version}-linux-x64.tar.xz", Location: "domestic"}, }, "helm": { {Name: "Helm官方", URL: "https://get.helm.sh/helm-{version}-linux-amd64.tar.gz", Location: "global"}, {Name: "阿里云镜像", URL: "https://mirrors.aliyun.com/kubernetes/helm/{version}/helm-{version}-linux-amd64.tar.gz", Location: "domestic"}, }, "golang": { {Name: "Go官方", URL: "https://go.dev/dl/go{version}.linux-amd64.tar.gz", Location: "global"}, {Name: "Google CN", URL: "https://golang.google.cn/dl/go{version}.linux-amd64.tar.gz", Location: "domestic"}, {Name: "阿里云镜像", URL: "https://mirrors.aliyun.com/golang/go{version}.linux-amd64.tar.gz", Location: "domestic"}, {Name: "中科大镜像", URL: "https://mirrors.ustc.edu.cn/golang/go{version}.linux-amd64.tar.gz", Location: "domestic"}, }, } // GetMirrorsForTool 返回工具的镜像源列表(按网络环境排序)。 // 国内网络 → 国内源优先;国际网络 → 官方源优先。 // 同时过滤掉不可达的镜像。 func GetMirrorsForTool(toolName, downloadURL string) []string { mirrors, ok := toolMirrors[toolName] if !ok { // 未配置镜像列表的工具,只返回 downloadURL if downloadURL != "" { return []string{downloadURL} } return nil } isChina := IsChinaNetwork() // 排序:国内网络 → 国内源排前面;国际网络 → 官方源排前面 sorted := make([]ToolMirror, len(mirrors)) copy(sorted, mirrors) // 冒泡排序(简单场景足够) for i := 0; i < len(sorted); i++ { for j := i + 1; j < len(sorted); j++ { // 国内网络:domestic 优先 if isChina && sorted[j].Location == "domestic" && sorted[i].Location != "domestic" { sorted[i], sorted[j] = sorted[j], sorted[i] } // 国际网络:global 优先 if !isChina && sorted[j].Location == "global" && sorted[i].Location != "global" { sorted[i], sorted[j] = sorted[j], sorted[i] } } } // 过滤掉不可达的镜像(HEAD 验证) urls := make([]string, 0, len(sorted)) for _, m := range sorted { if verifyURLReachable(m.URL) { urls = append(urls, m.URL) log.Printf("[ToolMirror] ✓ %s (%s) reachable", m.Name, m.URL) } else { log.Printf("[ToolMirror] ✗ %s (%s) unreachable, skip", m.Name, m.URL) } } // 如果所有镜像都不可达,仍然返回全部(让下载阶段报错,而非提前过滤掉) if len(urls) == 0 { log.Printf("[ToolMirror] WARNING: all mirrors unreachable for %s, falling back to raw list", toolName) for _, m := range sorted { urls = append(urls, m.URL) } } // 把 downloadURL 也作为兜底放在最后(如果不在 mirrors 中) if downloadURL != "" && !containsURL(urls, downloadURL) { urls = append(urls, downloadURL) } return urls } // verifyURLReachable 通过 HEAD 请求(带重定向跟随)验证 URL 是否可达。 // 返回 true 表示 URL 可访问(HTTP 2xx/3xx)。 func verifyURLReachable(urlTemplate string) bool { // 对 URL 模板做简单替换,用已知存在的版本测试 testURL := strings.ReplaceAll(urlTemplate, "{version}", "v1.32.0") testURL = strings.ReplaceAll(testURL, "{version}", "3.9.9") // maven 版本格式不同 client := &http.Client{ Timeout: 5 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { if len(via) >= 3 { return http.ErrUseLastResponse } return nil }, } resp, err := client.Head(testURL) if err != nil { return false } defer resp.Body.Close() // 2xx 或 3xx 都算可达 return resp.StatusCode >= 200 && resp.StatusCode < 400 } func containsURL(urls []string, target string) bool { for _, u := range urls { if u == target { return true } } return false } // VerifyAndGetBestMirror 综合检测:先 HEAD 测试所有镜像,返回第一个可达的。 // 如果全不可达,返回空字符串。 func VerifyAndGetBestMirror(toolName string, mirrors []string) string { for _, url := range mirrors { if verifyURLReachable(url) { return url } } log.Printf("[ToolMirror] WARNING: no reachable mirror found for %s", toolName) return "" }