package controller import ( "archive/tar" "bytes" "fmt" "io" "mime" "mime/multipart" "net/http" "os" "path" "path/filepath" "strings" "github.com/goodrain/rainbond/api/client/prometheus" "github.com/goodrain/rainbond/api/proxy" "github.com/goodrain/rainbond/api/util" dbmodel "github.com/goodrain/rainbond/db/model" "github.com/goodrain/rainbond/pkg/component/k8s" "github.com/goodrain/rainbond/pkg/component/storage" "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" clientset "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/client-go/tools/remotecommand" "github.com/go-chi/chi" "github.com/goodrain/rainbond/api/handler" api_model "github.com/goodrain/rainbond/api/model" ctxutil "github.com/goodrain/rainbond/api/util/ctx" httputil "github.com/goodrain/rainbond/util/http" ) // AddServiceMonitors add service monitor func (t *TenantStruct) AddServiceMonitors(w http.ResponseWriter, r *http.Request) { var add api_model.AddServiceMonitorRequestStruct ok := httputil.ValidatorRequestStructAndErrorResponse(r, w, &add, nil) if !ok { return } serviceID := r.Context().Value(ctxutil.ContextKey("service_id")).(string) tenantID := r.Context().Value(ctxutil.ContextKey("tenant_id")).(string) tsm, err := handler.GetServiceManager().AddServiceMonitor(tenantID, serviceID, add) if err != nil { httputil.ReturnBcodeError(r, w, err) return } httputil.ReturnSuccess(r, w, tsm) } // DeleteServiceMonitors delete service monitor func (t *TenantStruct) DeleteServiceMonitors(w http.ResponseWriter, r *http.Request) { serviceID := r.Context().Value(ctxutil.ContextKey("service_id")).(string) tenantID := r.Context().Value(ctxutil.ContextKey("tenant_id")).(string) name := chi.URLParam(r, "name") tsm, err := handler.GetServiceManager().DeleteServiceMonitor(tenantID, serviceID, name) if err != nil { httputil.ReturnBcodeError(r, w, err) return } httputil.ReturnSuccess(r, w, tsm) } // UpdateServiceMonitors update service monitor func (t *TenantStruct) UpdateServiceMonitors(w http.ResponseWriter, r *http.Request) { var update api_model.UpdateServiceMonitorRequestStruct ok := httputil.ValidatorRequestStructAndErrorResponse(r, w, &update, nil) if !ok { return } name := chi.URLParam(r, "name") serviceID := r.Context().Value(ctxutil.ContextKey("service_id")).(string) tenantID := r.Context().Value(ctxutil.ContextKey("tenant_id")).(string) tsm, err := handler.GetServiceManager().UpdateServiceMonitor(tenantID, serviceID, name, update) if err != nil { httputil.ReturnBcodeError(r, w, err) return } httputil.ReturnSuccess(r, w, tsm) } // UploadPackage upload package func (t *TenantStruct) UploadPackage(w http.ResponseWriter, r *http.Request) { SetPackageBuildCORSHeaders(w, r) eventID := strings.TrimSpace(chi.URLParam(r, "eventID")) switch r.Method { case "POST": if eventID == "" { httputil.ReturnError(r, w, 400, "Failed to parse eventID.") return } logrus.Debug("Start receive upload file: ", eventID) reader, header, err := r.FormFile("packageTarFile") if err != nil { logrus.Errorf("Failed to parse upload file: %s", err.Error()) httputil.ReturnError(r, w, 501, "Failed to parse upload file.") return } defer reader.Close() dirName := fmt.Sprintf("/grdata/package_build/temp/events/%s", eventID) storage.Default().StorageCli.MkdirAll(dirName) fileName := fmt.Sprintf("%s/%s", dirName, header.Filename) err = storage.Default().StorageCli.SaveFile(fileName, reader) if err != nil { httputil.ReturnError(r, w, 503, "Failed to save file: "+err.Error()) } logrus.Debug("successful write file to: ", fileName) httputil.ReturnSuccess(r, w, nil) case "OPTIONS": httputil.ReturnSuccess(r, w, nil) } } // GetMonitorMetrics get monitor metrics func GetMonitorMetrics(w http.ResponseWriter, r *http.Request) { target := r.FormValue("target") var metricMetadatas []prometheus.Metadata if target == "tenant" { metricMetadatas = handler.GetMonitorHandle().GetTenantMonitorMetrics(r.FormValue("tenant")) } if target == "app" { metricMetadatas = handler.GetMonitorHandle().GetAppMonitorMetrics(r.FormValue("tenant"), r.FormValue("app")) } if target == "component" { metricMetadatas = handler.GetMonitorHandle().GetComponentMonitorMetrics(r.FormValue("tenant"), r.FormValue("component")) } httputil.ReturnSuccess(r, w, metricMetadatas) } var fileManage *FileManage // FileManage docker log type FileManage struct { socketproxy proxy.Proxy clientset *clientset.Clientset config *rest.Config } // GetFileManage get docker log func GetFileManage() *FileManage { if fileManage == nil { fileManage = &FileManage{ socketproxy: proxy.CreateProxy("acp_node", "http", []string{"rbd-node:6100"}), clientset: k8s.Default().Clientset, config: k8s.Default().RestConfig, } //discover.GetEndpointDiscover().AddProject("acp_node", fileManage.socketproxy) } return fileManage } // UploadEvent volume file upload event func (f FileManage) UploadEvent(w http.ResponseWriter, r *http.Request) { volumeName := w.Header().Get("volume_name") userName := w.Header().Get("user_name") tenantID := w.Header().Get("tenant_id") serviceID := w.Header().Get("service_id") fileName := w.Header().Get("file_name") status := w.Header().Get("status") msg := fmt.Sprintf("%v to upload file %v in storage %v", status, fileName, volumeName) _, err := util.CreateEvent(dbmodel.TargetTypeService, "volume-file-upload", serviceID, tenantID, "", userName, status, msg, 1) if err != nil { logrus.Error("create event error: ", err) httputil.ReturnError(r, w, 500, "操作失败") } httputil.ReturnSuccess(r, w, nil) } // UploadFile uploads files to a container in the selected pod. func (f FileManage) UploadFile(w http.ResponseWriter, r *http.Request) { // 设置 CORS 头 origin := r.Header.Get("Origin") if origin == "" { origin = "*" } w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Custom-Header") w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Access-Control-Max-Age", "3600") // 处理预检请求 if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } logrus.Debugf("开始处理文件上传请求,Method: %s, ContentType: %s, Origin: %s", r.Method, r.Header.Get("Content-Type"), origin) // 设置响应头 w.Header().Set("volume_name", r.FormValue("volume_name")) w.Header().Set("user_name", r.FormValue("user_name")) w.Header().Set("tenant_id", r.FormValue("tenant_id")) w.Header().Set("service_id", r.FormValue("service_id")) w.Header().Set("status", "failed") // 获取上传参数 destPath := r.FormValue("path") podName := r.FormValue("pod_name") namespace := r.FormValue("namespace") containerName := r.FormValue("container_name") logrus.Debugf("上传参数: destPath=%s, podName=%s, namespace=%s, containerName=%s", destPath, podName, namespace, containerName) if destPath == "" { httputil.ReturnError(r, w, 400, "目标路径不能为空") return } // 解析multipart form数据 err := r.ParseMultipartForm(32 << 20) // 32MB if err != nil { logrus.Errorf("解析multipart form失败: %v", err) httputil.ReturnError(r, w, 400, fmt.Sprintf("解析上传数据失败: %v", err)) return } // 检查form是否为nil if r.MultipartForm == nil { logrus.Error("MultipartForm为空") httputil.ReturnError(r, w, 400, "未找到上传文件") return } // 打印所有可用的form字段 logrus.Debugf("Form字段: %+v", r.MultipartForm.Value) logrus.Debugf("文件字段: %+v", r.MultipartForm.File) // 尝试获取文件 var files []*multipart.FileHeader if formFiles := r.MultipartForm.File["files"]; len(formFiles) > 0 { files = formFiles } else if formFiles := r.MultipartForm.File["file"]; len(formFiles) > 0 { files = formFiles } if len(files) == 0 { logrus.Error("未找到上传文件") httputil.ReturnError(r, w, 400, "未找到上传文件") return } // 创建临时目录存储上传的文件 tempDir, err := os.MkdirTemp("", "upload-*") if err != nil { logrus.Errorf("创建临时目录失败: %v", err) httputil.ReturnError(r, w, 500, fmt.Sprintf("创建临时目录失败: %v", err)) return } defer os.RemoveAll(tempDir) isDirectoryUpload := len(files) > 1 // 保存文件到临时目录 for _, fileHeader := range files { logrus.Debugf("处理文件: %s", fileHeader.Filename) relativePath, hasDirectoryPath, err := resolveUploadRelativePath(fileHeader) if err != nil { logrus.Errorf("上传文件路径非法: %v", err) httputil.ReturnError(r, w, 400, fmt.Sprintf("上传文件路径非法: %v", err)) return } isDirectoryUpload = isDirectoryUpload || hasDirectoryPath logrus.Debugf("上传文件相对路径: %s", relativePath) file, err := fileHeader.Open() if err != nil { logrus.Errorf("打开上传文件失败: %v", err) httputil.ReturnError(r, w, 500, fmt.Sprintf("打开上传文件失败: %v", err)) return } defer file.Close() // 构建临时目录中的完整路径 tempPath := filepath.Join(tempDir, relativePath) logrus.Debugf("临时文件路径: %s", tempPath) // 确保目标目录存在 if err := os.MkdirAll(filepath.Dir(tempPath), 0755); err != nil { logrus.Errorf("创建目录失败: %v", err) httputil.ReturnError(r, w, 500, fmt.Sprintf("创建目录失败: %v", err)) return } dst, err := os.Create(tempPath) if err != nil { logrus.Errorf("创建文件失败: %v", err) httputil.ReturnError(r, w, 500, fmt.Sprintf("创建文件失败: %v", err)) return } defer dst.Close() if _, err = io.Copy(dst, file); err != nil { logrus.Errorf("保存文件失败: %v", err) httputil.ReturnError(r, w, 500, fmt.Sprintf("保存文件失败: %v", err)) return } logrus.Debugf("成功保存文件: %s", tempPath) } // 上传文件到容器 logrus.Debugf("开始上传文件到容器,源目录: %s,目标路径: %s", tempDir, destPath) // 获取第一个文件的目录结构信息来判断是单文件还是目录上传 firstFile := files[0] if len(files) == 1 && !isDirectoryUpload { // 如果是单文件上传 uploadPath := filepath.Join(tempDir, firstFile.Filename) err = f.AppFileUpload(containerName, podName, uploadPath, destPath, namespace) } else { // 目录或多文件上传 err = f.AppFileUpload(containerName, podName, tempDir, destPath, namespace) } if err != nil { logrus.Errorf("上传到容器失败: %v", err) httputil.ReturnError(r, w, 500, fmt.Sprintf("上传失败: %v", err)) return } logrus.Debug("文件上传成功") w.Header().Set("status", "success") httputil.ReturnSuccess(r, w, nil) } func resolveUploadRelativePath(fileHeader *multipart.FileHeader) (string, bool, error) { relativePath := "" if contentDisposition := fileHeader.Header.Get("Content-Disposition"); contentDisposition != "" { if _, params, err := mime.ParseMediaType(contentDisposition); err == nil { relativePath = params["filename"] } } if relativePath == "" { relativePath = fileHeader.Header.Get("webkitRelativePath") } if relativePath == "" { relativePath = fileHeader.Filename } relativePath = strings.ReplaceAll(relativePath, "\\", "/") for _, part := range strings.Split(relativePath, "/") { if part == ".." { return "", false, fmt.Errorf("invalid relative path %q", relativePath) } } cleaned := path.Clean(relativePath) if cleaned == "." || cleaned == "/" || strings.HasPrefix(cleaned, "../") || cleaned == ".." || path.IsAbs(cleaned) { return "", false, fmt.Errorf("invalid relative path %q", relativePath) } return cleaned, strings.Contains(cleaned, "/"), nil } // DownloadFile serves a file downloaded from a container in the selected pod. func (f FileManage) DownloadFile(w http.ResponseWriter, r *http.Request) { logrus.Debugf("接收到文件下载请求: Method=%s, ContentType=%s", r.Method, r.Header.Get("Content-Type")) // 设置响应头 w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("volume_name", r.FormValue("volume_name")) w.Header().Set("user_name", r.FormValue("user_name")) w.Header().Set("tenant_id", r.FormValue("tenant_id")) w.Header().Set("service_id", r.FormValue("service_id")) w.Header().Set("status", "failed") // 获取请求参数 podName := r.FormValue("pod_name") p := r.FormValue("path") namespace := r.FormValue("namespace") fileName := strings.TrimSpace(chi.URLParam(r, "fileName")) containerName := r.FormValue("container_name") logrus.Debugf("下载文件参数: podName=%s, path=%s, namespace=%s, fileName=%s, containerName=%s", podName, p, namespace, fileName, containerName) if fileName == "" { logrus.Error("文件名为空") httputil.ReturnError(r, w, 400, "文件名不能为空") return } filePath := path.Join(p, fileName) logrus.Debugf("完整文件路径: %s", filePath) err := f.AppFileDownload(containerName, podName, filePath, namespace) if err != nil { logrus.Errorf("从Pod下载文件失败: %v", err) httputil.ReturnError(r, w, 500, fmt.Sprintf("下载文件失败: %v", err)) return } defer func() { if err := os.Remove(path.Join("./", fileName)); err != nil { logrus.Errorf("删除临时文件失败: %v", err) } }() logrus.Debugf("开始传输文件: %s", fileName) if err := serveDownloadedFile(w, r, fileName); err != nil { logrus.Errorf("读取下载文件失败: %v", err) httputil.ReturnError(r, w, 500, fmt.Sprintf("下载文件失败: %v", err)) } } // AppFileDownload copies a file or directory from a container to local storage. func (f FileManage) AppFileDownload(containerName, podName, filePath, namespace string) error { // Check if the file exists first checkCmd := []string{"test", "-e", filePath} req := f.clientset.CoreV1().RESTClient().Get(). Resource("pods"). Name(podName). Namespace(namespace). SubResource("exec"). VersionedParams(&corev1.PodExecOptions{ Container: containerName, Command: checkCmd, Stdin: false, Stdout: true, Stderr: true, TTY: false, }, scheme.ParameterCodec) exec, err := remotecommand.NewSPDYExecutor(f.config, "POST", req.URL()) if err != nil { return fmt.Errorf("failed to create executor: %v", err) } var checkStdout, checkStderr bytes.Buffer err = exec.Stream(remotecommand.StreamOptions{ Stdout: &checkStdout, Stderr: &checkStderr, }) if err != nil { return fmt.Errorf("file not found: %s", filePath) } // Check if file is a directory isDirCmd := []string{"test", "-d", filePath} req = f.clientset.CoreV1().RESTClient().Get(). Resource("pods"). Name(podName). Namespace(namespace). SubResource("exec"). VersionedParams(&corev1.PodExecOptions{ Container: containerName, Command: isDirCmd, Stdin: false, Stdout: true, Stderr: true, TTY: false, }, scheme.ParameterCodec) exec, err = remotecommand.NewSPDYExecutor(f.config, "POST", req.URL()) if err != nil { return fmt.Errorf("failed to create executor: %v", err) } var isDirStdout, isDirStderr bytes.Buffer err = exec.Stream(remotecommand.StreamOptions{ Stdout: &isDirStdout, Stderr: &isDirStderr, }) isDirectory := (err == nil) // For single files, try to use cat command (no tar dependency) if !isDirectory { return f.downloadFileUsingCat(containerName, podName, filePath, namespace) } // For directories, try tar first, fallback to cat if tar is not available err = f.downloadUsingTar(containerName, podName, filePath, namespace) if err != nil && strings.Contains(err.Error(), "tar") { logrus.Warnf("tar command not available, falling back to simple file copy: %v", err) return f.downloadFileUsingCat(containerName, podName, filePath, namespace) } return err } func (f FileManage) downloadFileUsingCat(containerName, podName, filePath, namespace string) error { req := f.clientset.CoreV1().RESTClient().Get(). Resource("pods"). Name(podName). Namespace(namespace). SubResource("exec"). VersionedParams(&corev1.PodExecOptions{ Container: containerName, Command: []string{"cat", filePath}, Stdin: false, Stdout: true, Stderr: true, TTY: false, }, scheme.ParameterCodec) exec, err := remotecommand.NewSPDYExecutor(f.config, "POST", req.URL()) if err != nil { return fmt.Errorf("failed to create executor: %v", err) } fileName := path.Base(filePath) destPath := path.Join("./", fileName) outFile, err := os.Create(destPath) if err != nil { return fmt.Errorf("failed to create local file: %v", err) } defer outFile.Close() var stderr bytes.Buffer err = exec.Stream(remotecommand.StreamOptions{ Stdout: outFile, Stderr: &stderr, Tty: false, }) if err != nil { return fmt.Errorf("failed to download file using cat: %v, stderr: %s", err, stderr.String()) } logrus.Debugf("Successfully downloaded file %s using cat command", fileName) return nil } func (f FileManage) downloadUsingTar(containerName, podName, filePath, namespace string) error { reader, outStream := io.Pipe() req := f.clientset.CoreV1().RESTClient().Get(). Resource("pods"). Name(podName). Namespace(namespace). SubResource("exec"). VersionedParams(&corev1.PodExecOptions{ Container: containerName, Command: []string{"tar", "cf", "-", filePath}, Stdin: true, Stdout: true, Stderr: true, TTY: false, }, scheme.ParameterCodec) exec, err := remotecommand.NewSPDYExecutor(f.config, "POST", req.URL()) if err != nil { return fmt.Errorf("failed to create executor: %v", err) } go func() { defer outStream.Close() err = exec.Stream(remotecommand.StreamOptions{ Stdin: os.Stdin, Stdout: outStream, Stderr: os.Stderr, Tty: false, }) if err != nil { logrus.Errorf("stream error: %v", err) } }() prefix := getPrefix(filePath) prefix = path.Clean(prefix) destPath := path.Join("./", path.Base(prefix)) err = unTarAll(reader, destPath, prefix) if err != nil { return fmt.Errorf("failed to untar: %v", err) } return nil } // AppFileUpload copies a local file or directory into a container. func (f FileManage) AppFileUpload(containerName, podName, srcPath, destPath, namespace string) error { logrus.Debugf("开始上传目录/文件: 源路径=%s, 目标路径=%s", srcPath, destPath) // 检查源路径是否存在 srcInfo, err := os.Stat(srcPath) if err != nil { return fmt.Errorf("源路径检查失败: %v", err) } // For single files, use tee command (no tar dependency) if !srcInfo.IsDir() { return f.uploadFileUsingTee(containerName, podName, srcPath, destPath, namespace) } // For directories, try tar first, fallback to recursive tee if tar is not available err = f.uploadUsingTar(containerName, podName, srcPath, destPath, namespace) if err != nil && strings.Contains(err.Error(), "tar") { logrus.Warnf("tar command not available, falling back to recursive file copy: %v", err) return f.uploadDirectoryUsingTee(containerName, podName, srcPath, destPath, namespace) } return err } // uploadUsingTar uploads files/directories using tar command func (f FileManage) uploadUsingTar(containerName, podName, srcPath, destPath, namespace string) error { srcInfo, err := os.Stat(srcPath) if err != nil { return fmt.Errorf("源路径检查失败: %v", err) } reader, writer := io.Pipe() // 在goroutine中处理tar打包 go func() { defer writer.Close() // 如果是目录,使用完整目录路径 if srcInfo.IsDir() { logrus.Debugf("正在打包目录: %s", srcPath) err = cpMakeTar(srcPath, destPath, writer) } else { logrus.Debugf("正在打包文件: %s", srcPath) err = cpMakeTar(filepath.Dir(srcPath), destPath, writer) } if err != nil { logrus.Errorf("tar打包失败: %v", err) writer.CloseWithError(err) return } }() // 构建在容器中解压的命令 var cmdArr []string cmdArr = []string{"tar", "-xmf", "-"} // 确保目标路径存在 if len(destPath) > 0 { // 先创建目标目录 mkdirCmd := []string{"mkdir", "-p", destPath} mkdirReq := f.clientset.CoreV1().RESTClient().Post(). Resource("pods"). Name(podName). Namespace(namespace). SubResource("exec"). VersionedParams(&corev1.PodExecOptions{ Container: containerName, Command: mkdirCmd, Stdin: false, Stdout: true, Stderr: true, TTY: false, }, scheme.ParameterCodec) mkdirExec, err := remotecommand.NewSPDYExecutor(f.config, "POST", mkdirReq.URL()) if err != nil { return fmt.Errorf("创建目标目录执行器失败: %v", err) } if err := mkdirExec.Stream(remotecommand.StreamOptions{ Stdout: os.Stdout, Stderr: os.Stderr, }); err != nil { return fmt.Errorf("在容器中创建目录失败: %v", err) } cmdArr = append(cmdArr, "-C", destPath) } // 执行解压命令 req := f.clientset.CoreV1().RESTClient().Post(). Resource("pods"). Name(podName). Namespace(namespace). SubResource("exec"). VersionedParams(&corev1.PodExecOptions{ Container: containerName, Command: cmdArr, Stdin: true, Stdout: true, Stderr: true, TTY: false, }, scheme.ParameterCodec) exec, err := remotecommand.NewSPDYExecutor(f.config, "POST", req.URL()) if err != nil { return fmt.Errorf("创建解压执行器失败: %v", err) } logrus.Debug("开始在容器中解压文件") err = exec.Stream(remotecommand.StreamOptions{ Stdin: reader, Stdout: os.Stdout, Stderr: os.Stderr, Tty: false, }) if err != nil { return fmt.Errorf("在容器中解压失败: %v", err) } logrus.Debug("文件/目录上传完成") return nil } // uploadDirectoryUsingTee recursively uploads directory using tee command (no tar dependency) func (f FileManage) uploadDirectoryUsingTee(containerName, podName, srcPath, destPath, namespace string) error { logrus.Debugf("使用递归方式上传目录: %s -> %s", srcPath, destPath) return filepath.Walk(srcPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // 获取相对路径 relPath, err := filepath.Rel(srcPath, path) if err != nil { return fmt.Errorf("获取相对路径失败: %v", err) } // 构建目标路径 var targetPath string if relPath == "." { targetPath = destPath } else { targetPath = filepath.Join(destPath, relPath) } // 如果是目录,创建目录 if info.IsDir() { if relPath != "." { mkdirCmd := []string{"mkdir", "-p", targetPath} req := f.clientset.CoreV1().RESTClient().Post(). Resource("pods"). Name(podName). Namespace(namespace). SubResource("exec"). VersionedParams(&corev1.PodExecOptions{ Container: containerName, Command: mkdirCmd, Stdin: false, Stdout: true, Stderr: true, TTY: false, }, scheme.ParameterCodec) exec, err := remotecommand.NewSPDYExecutor(f.config, "POST", req.URL()) if err != nil { return fmt.Errorf("创建目录执行器失败: %v", err) } var stderr bytes.Buffer if err := exec.Stream(remotecommand.StreamOptions{ Stdout: &bytes.Buffer{}, Stderr: &stderr, }); err != nil { logrus.Warnf("创建目录警告: %v, stderr: %s", err, stderr.String()) } } return nil } // 如果是文件,使用 tee 上传 logrus.Debugf("上传文件: %s -> %s", path, targetPath) srcFile, err := os.Open(path) if err != nil { return fmt.Errorf("打开源文件失败: %v", err) } defer srcFile.Close() teeCmd := []string{"tee", targetPath} req := f.clientset.CoreV1().RESTClient().Post(). Resource("pods"). Name(podName). Namespace(namespace). SubResource("exec"). VersionedParams(&corev1.PodExecOptions{ Container: containerName, Command: teeCmd, Stdin: true, Stdout: true, Stderr: true, TTY: false, }, scheme.ParameterCodec) exec, err := remotecommand.NewSPDYExecutor(f.config, "POST", req.URL()) if err != nil { return fmt.Errorf("创建 tee 执行器失败: %v", err) } var stderr bytes.Buffer err = exec.Stream(remotecommand.StreamOptions{ Stdin: srcFile, Stdout: &bytes.Buffer{}, Stderr: &stderr, Tty: false, }) if err != nil { return fmt.Errorf("上传文件失败: %v, stderr: %s", err, stderr.String()) } return nil }) } // checkTarAvailable checks if tar command is available in the container func (f FileManage) checkTarAvailable(containerName, podName, namespace string) bool { checkCmd := []string{"tar", "--version"} req := f.clientset.CoreV1().RESTClient().Post(). Resource("pods"). Name(podName). Namespace(namespace). SubResource("exec"). VersionedParams(&corev1.PodExecOptions{ Container: containerName, Command: checkCmd, Stdin: false, Stdout: true, Stderr: true, TTY: false, }, scheme.ParameterCodec) exec, err := remotecommand.NewSPDYExecutor(f.config, "POST", req.URL()) if err != nil { return false } var stdout, stderr bytes.Buffer err = exec.Stream(remotecommand.StreamOptions{ Stdout: &stdout, Stderr: &stderr, }) // If tar command exists, it should succeed or show version info return err == nil } // uploadFileUsingTee uploads a single file using tee command (no tar dependency) func (f FileManage) uploadFileUsingTee(containerName, podName, srcPath, destPath, namespace string) error { // Read the source file srcFile, err := os.Open(srcPath) if err != nil { return fmt.Errorf("failed to open source file: %v", err) } defer srcFile.Close() // Get filename from path fileName := filepath.Base(srcPath) // Build destination path var fullPath string if strings.HasSuffix(destPath, "/") || destPath == "" { fullPath = path.Join(destPath, fileName) } else { // Check if destPath is a directory in the container isDirCmd := []string{"test", "-d", destPath} isDirReq := f.clientset.CoreV1().RESTClient().Post(). Resource("pods"). Name(podName). Namespace(namespace). SubResource("exec"). VersionedParams(&corev1.PodExecOptions{ Container: containerName, Command: isDirCmd, Stdin: false, Stdout: true, Stderr: true, TTY: false, }, scheme.ParameterCodec) isDirExec, err := remotecommand.NewSPDYExecutor(f.config, "POST", isDirReq.URL()) if err != nil { return fmt.Errorf("failed to create directory check executor: %v", err) } var isDirStdout, isDirStderr bytes.Buffer err = isDirExec.Stream(remotecommand.StreamOptions{ Stdout: &isDirStdout, Stderr: &isDirStderr, }) // If test -d succeeds (err == nil), it's a directory if err == nil { fullPath = path.Join(destPath, fileName) } else { fullPath = destPath } } // Create parent directory if needed dirPath := path.Dir(fullPath) if dirPath != "." && dirPath != "/" { mkdirCmd := []string{"mkdir", "-p", dirPath} req := f.clientset.CoreV1().RESTClient().Post(). Resource("pods"). Name(podName). Namespace(namespace). SubResource("exec"). VersionedParams(&corev1.PodExecOptions{ Container: containerName, Command: mkdirCmd, Stdin: false, Stdout: true, Stderr: true, TTY: false, }, scheme.ParameterCodec) exec, err := remotecommand.NewSPDYExecutor(f.config, "POST", req.URL()) if err != nil { return fmt.Errorf("failed to create mkdir executor: %v", err) } var stderr bytes.Buffer if err := exec.Stream(remotecommand.StreamOptions{ Stdout: &bytes.Buffer{}, Stderr: &stderr, }); err != nil { logrus.Warnf("mkdir warning: %v, stderr: %s", err, stderr.String()) } } // Use tee command to write file teeCmd := []string{"tee", fullPath} req := f.clientset.CoreV1().RESTClient().Post(). Resource("pods"). Name(podName). Namespace(namespace). SubResource("exec"). VersionedParams(&corev1.PodExecOptions{ Container: containerName, Command: teeCmd, Stdin: true, Stdout: true, Stderr: true, TTY: false, }, scheme.ParameterCodec) exec, err := remotecommand.NewSPDYExecutor(f.config, "POST", req.URL()) if err != nil { return fmt.Errorf("failed to create tee executor: %v", err) } var stderr bytes.Buffer err = exec.Stream(remotecommand.StreamOptions{ Stdin: srcFile, Stdout: &bytes.Buffer{}, Stderr: &stderr, Tty: false, }) if err != nil { return fmt.Errorf("failed to upload file using tee: %v, stderr: %s", err, stderr.String()) } logrus.Debugf("Successfully uploaded file %s using tee command", fileName) return nil } func cpMakeTar(srcPath string, destPath string, out io.Writer) error { tw := tar.NewWriter(out) defer tw.Close() // 获取源路径的信息 srcInfo, err := os.Stat(srcPath) if err != nil { return fmt.Errorf("failed to get info for %s: %v", srcPath, err) } // 确定基础路径 basePath := srcPath if srcInfo.IsDir() { // 如果是目录,使用目录本身作为基础路径 basePath = srcPath } else { // 如果是文件,使用父目录作为基础路径 basePath = filepath.Dir(srcPath) } return filepath.Walk(srcPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // 获取相对路径 relPath, err := filepath.Rel(basePath, path) if err != nil { return err } // 如果是根目录,跳过 if relPath == "." && srcInfo.IsDir() { return nil } // 创建header hdr, err := tar.FileInfoHeader(info, "") if err != nil { return fmt.Errorf("failed to create header for %s: %v", path, err) } // 修改header名称,使用相对路径 if srcInfo.IsDir() { // 如果源路径是目录,保持相对路径结构 hdr.Name = relPath } else { // 如果源路径是文件,直接使用文件名 hdr.Name = filepath.Base(srcPath) } if info.IsDir() { hdr.Name += "/" } logrus.Debugf("Adding to tar: %s", hdr.Name) // 写入header if err := tw.WriteHeader(hdr); err != nil { return fmt.Errorf("failed to write header for %s: %v", path, err) } // 如果是普通文件,写入文件内容 if !info.IsDir() { file, err := os.Open(path) if err != nil { return err } defer file.Close() if _, err := io.Copy(tw, file); err != nil { return fmt.Errorf("failed to write file %s: %v", path, err) } } return nil }) } func unTarAll(reader io.Reader, destDir, prefix string) error { tarReader := tar.NewReader(reader) for { header, err := tarReader.Next() if err != nil { if err != io.EOF { return err } break } if !strings.HasPrefix(header.Name, prefix) { return fmt.Errorf("tar contents corrupted") } mode := header.FileInfo().Mode() destFileName := filepath.Join(destDir, header.Name[len(prefix):]) baseName := filepath.Dir(destFileName) if err := os.MkdirAll(baseName, 0755); err != nil { return err } if header.FileInfo().IsDir() { if err := os.MkdirAll(destFileName, 0755); err != nil { return err } continue } evaledPath, err := filepath.EvalSymlinks(baseName) if err != nil { return err } if mode&os.ModeSymlink != 0 { linkname := header.Linkname if !filepath.IsAbs(linkname) { _ = filepath.Join(evaledPath, linkname) } if err := os.Symlink(linkname, destFileName); err != nil { return err } } else { outFile, err := os.Create(destFileName) if err != nil { return err } defer outFile.Close() if _, err := io.Copy(outFile, tarReader); err != nil { return err } if err := outFile.Close(); err != nil { return err } } } return nil } func getPrefix(file string) string { return strings.TrimLeft(file, "/") } // CreateDirectory 在容器内创建目录 func (f FileManage) CreateDirectory(w http.ResponseWriter, r *http.Request) { logrus.Debugf("接收到创建目录请求: Method=%s, ContentType=%s", r.Method, r.Header.Get("Content-Type")) // 获取请求参数 podName := r.FormValue("pod_name") namespace := r.FormValue("namespace") containerName := r.FormValue("container_name") dirPath := r.FormValue("path") logrus.Debugf("创建目录参数: podName=%s, namespace=%s, containerName=%s, dirPath=%s", podName, namespace, containerName, dirPath) if dirPath == "" { logrus.Error("目录路径为空") httputil.ReturnError(r, w, 400, "目录路径不能为空") return } // 构建在容器中创建目录的命令 req := f.clientset.CoreV1().RESTClient().Post(). Resource("pods"). Name(podName). Namespace(namespace). SubResource("exec"). VersionedParams(&corev1.PodExecOptions{ Container: containerName, Command: []string{"mkdir", "-p", dirPath}, Stdin: false, Stdout: true, Stderr: true, TTY: false, }, scheme.ParameterCodec) exec, err := remotecommand.NewSPDYExecutor(f.config, "POST", req.URL()) if err != nil { logrus.Errorf("创建目录执行器失败: %v", err) httputil.ReturnError(r, w, 500, fmt.Sprintf("创建目录失败: %v", err)) return } // 创建缓冲区来捕获输出 var stdout, stderr bytes.Buffer err = exec.Stream(remotecommand.StreamOptions{ Stdin: nil, Stdout: &stdout, Stderr: &stderr, Tty: false, }) // 记录命令输出 if stdout.Len() > 0 { logrus.Debugf("命令输出: %s", stdout.String()) } if stderr.Len() > 0 { logrus.Debugf("错误输出: %s", stderr.String()) } if err != nil { logrus.Errorf("在容器中创建目录失败: %v", err) httputil.ReturnError(r, w, 500, fmt.Sprintf("创建目录失败: %v", err)) return } logrus.Debugf("目录创建成功: %s", dirPath) httputil.ReturnSuccess(r, w, nil) }