// Copyright 2020 the Drone Authors. All rights reserved. // Use of this source code is governed by the Blue Oak Model License // that can be found in the LICENSE file. package plugin import ( "bytes" "context" "fmt" "gopkg.in/yaml.v2" "io" "mime/multipart" "net/http" "os" "path/filepath" "regexp" "strings" ) type Plugin interface { Init(args *Args) error SetBuildRoot(buildRootPath string) error DeInit() error ValidateAndProcessArgs(args Args) error DoPostArgsValidationSetup(args Args) error Run() error WriteOutputVariables() error PersistResults() error IsQuiet() bool InspectProcessArgs(argNamesList []string) (map[string]interface{}, error) } type Args struct { EnvPluginInputArgs Level string `envconfig:"PLUGIN_LOG_LEVEL"` } type EnvPluginInputArgs struct { NexusVersion string `envconfig:"PLUGIN_NEXUS_VERSION"` Protocol string `envconfig:"PLUGIN_PROTOCOL"` GroupId string `envconfig:"PLUGIN_GROUP_ID"` Repository string `envconfig:"PLUGIN_REPOSITORY"` Artifact string `envconfig:"PLUGIN_ARTIFACTS"` Username string `envconfig:"PLUGIN_USERNAME"` Password string `envconfig:"PLUGIN_PASSWORD"` // For backward compatibility ServerUrl string `envconfig:"PLUGIN_SERVER_URL"` Filename string `envconfig:"PLUGIN_FILENAME"` Format string `envconfig:"PLUGIN_FORMAT"` Attributes string `envconfig:"PLUGIN_ATTRIBUTES"` } type Artifact struct { File string `yaml:"file"` Classifier string `yaml:"classifier"` ArtifactId string `yaml:"artifactId"` Type string `yaml:"type"` Version string `yaml:"version"` GroupId string `yaml:"groupId"` } func GetNewPlugin(ctx context.Context, args Args) (Plugin, error) { nxp := GetNewNexusPlugin() return &nxp, nil } func Exec(ctx context.Context, args Args) (Plugin, error) { plugin, err := GetNewPlugin(ctx, args) if err != nil { return plugin, err } err = plugin.Init(&args) if err != nil { return plugin, err } defer func(p Plugin) { err := p.DeInit() if err != nil { LogPrintln(p, "Error in DeInit: "+err.Error()) } }(plugin) err = plugin.ValidateAndProcessArgs(args) if err != nil { return plugin, err } err = plugin.DoPostArgsValidationSetup(args) if err != nil { return plugin, err } err = plugin.Run() err2 := plugin.WriteOutputVariables() if err2 != nil { LogPrintln(plugin, "Writing output variable UPLOAD_STATUS failed "+err2.Error()) } if err != nil { LogPrintln(plugin, "Upload failed "+err.Error()) return plugin, err } err = plugin.PersistResults() if err != nil { return plugin, err } return plugin, nil } type HttpClient interface { Do(req *http.Request) (*http.Response, error) } type NexusPlugin struct { InputArgs *Args IsMultiFileUpload bool PluginProcessingInfo NexusPluginResponse HttpClient HttpClient } type PluginProcessingInfo struct { UserName string Password string ServerUrl string Version string Format string Repository string GroupId string Artifacts []Artifact } type NexusPluginResponse struct { Failed []FailedArtifact `json:"failed"` } type FailedArtifact struct { File string `json:"file"` ArtifactId string `json:"artifactId"` Err string `json:"err"` } func (n *NexusPlugin) Run() error { LogPrintln(n, "Starting Nexus Plugin Run") if n.HttpClient == nil { n.HttpClient = &http.Client{} } // Log upload configuration summary LogPrintln(n, "") LogPrintln(n, "Upload Configuration:") LogPrintln(n, fmt.Sprintf(" Nexus Version: %s", n.Version)) LogPrintln(n, fmt.Sprintf(" Server URL: %s", n.ServerUrl)) LogPrintln(n, fmt.Sprintf(" Repository: %s", n.Repository)) LogPrintln(n, fmt.Sprintf(" Format: %s", n.Format)) LogPrintln(n, fmt.Sprintf(" Total artifacts: %d", len(n.Artifacts))) LogPrintln(n, "") for idx, artifact := range n.Artifacts { filePath := artifact.File file, err := os.Open(filePath) if err != nil { n.addFailedArtifact(artifact, fmt.Sprintf("could not open file: %v", err)) continue } // Log individual artifact details before upload LogPrintln(n, fmt.Sprintf("Uploading artifact %d/%d:", idx+1, len(n.Artifacts))) // Get file size from the opened file handle fileInfo, statErr := file.Stat() var sizeStr string if statErr == nil { fileSize := float64(fileInfo.Size()) / (1024 * 1024) // Convert to MB sizeStr = fmt.Sprintf(" (%.2f MB)", fileSize) } LogPrintln(n, fmt.Sprintf(" File: %s%s", filePath, sizeStr)) LogPrintln(n, fmt.Sprintf(" ArtifactId: %s", artifact.ArtifactId)) if artifact.GroupId != "" { LogPrintln(n, fmt.Sprintf(" GroupId: %s", artifact.GroupId)) } LogPrintln(n, fmt.Sprintf(" Version: %s", artifact.Version)) LogPrintln(n, fmt.Sprintf(" Type: %s", artifact.Type)) if artifact.Classifier != "" { LogPrintln(n, fmt.Sprintf(" Classifier: %s", artifact.Classifier)) } if n.Version == "nexus2" { artifactURL := n.prepareNexus2ArtifactURL(artifact) if err := n.uploadFileNexus2(artifactURL, file, filePath); err != nil { n.addFailedArtifact(artifact, fmt.Sprintf("upload failed: %v", err)) err := file.Close() if err != nil { LogPrintln(n, "Error closing file: ", err.Error()) } continue } } else if n.Version == "nexus3" { if err := n.uploadFileNexus3(artifact, filePath); err != nil { n.addFailedArtifact(artifact, fmt.Sprintf("upload failed: %v", err)) err := file.Close() if err != nil { LogPrintln(n, "Error closing file: ", err.Error()) } continue } } err = file.Close() if err != nil { LogPrintln(n, "Error closing file: ", err.Error()) } // Log enhanced success message with artifact coordinates basename := filepath.Base(filePath) coordinates := fmt.Sprintf("%s:%s:%s", artifact.GroupId, artifact.ArtifactId, artifact.Version) if artifact.GroupId == "" { coordinates = fmt.Sprintf("%s:%s", artifact.ArtifactId, artifact.Version) } LogPrintln(n, fmt.Sprintf("[OK] Successfully uploaded: %s -> %s", basename, coordinates)) LogPrintln(n, "") } // Log upload summary totalArtifacts := len(n.Artifacts) successCount := totalArtifacts - len(n.Failed) LogPrintln(n, "Upload Summary:") LogPrintln(n, fmt.Sprintf(" Total: %d, Successful: %d, Failed: %d", totalArtifacts, successCount, len(n.Failed))) if len(n.Failed) > 0 { return GetNewError("NexusPlugin Error in Run: some artifacts failed to upload") } return nil } func (n *NexusPlugin) WriteOutputVariables() error { type EnvKvPair struct { Key string Value interface{} } var kvPairs []EnvKvPair if len(n.Failed) == 0 { LogPrintln(n, "All artifacts uploaded successfully") kvPairs = append(kvPairs, EnvKvPair{Key: "UPLOAD_STATUS", Value: "Success"}) } else { kvPairs = append(kvPairs, EnvKvPair{Key: "UPLOAD_STATUS", Value: n.Failed}) } var retErr error = nil for _, kvPair := range kvPairs { err := WriteEnvVariableAsString(kvPair.Key, kvPair.Value) if err != nil { retErr = err } } return retErr } func (n *NexusPlugin) Init(args *Args) error { n.InputArgs = args return nil } func (n *NexusPlugin) SetBuildRoot(buildRootPath string) error { return nil } func (n *NexusPlugin) DeInit() error { return nil } func (n *NexusPlugin) ValidateAndProcessArgs(args Args) error { LogPrintln(n, "NexusPlugin BuildAndValidateArgs") err := n.DetermineIsMultiFileUpload(args) if err != nil { LogPrintln(n, "NexusPlugin Error in ValidateAndProcessArgs: "+err.Error()) return err } if n.IsMultiFileUpload { err = n.IsMultiFileUploadArgsOk(args) if err != nil { LogPrintln(n, "NexusPlugin Error in ValidateAndProcessArgs: "+err.Error()) return err } } else { err = n.IsSingleFileUploadArgsOk(args) if err != nil { LogPrintln(n, "NexusPlugin Error in ValidateAndProcessArgs: "+err.Error()) return err } } return nil } func (n *NexusPlugin) DetermineIsMultiFileUpload(args Args) error { LogPrintln(n, "NexusPlugin DetermineIsMultiFileUpload") switch { case args.Attributes != "" && args.Artifact == "": n.IsMultiFileUpload = false case args.Artifact != "" && args.Attributes == "": n.IsMultiFileUpload = true case args.Attributes == "" && args.Artifact == "": return GetNewError("Error in DetermineCompatibilityMode: both 'Attributes' and 'Artifact' cannot be empty") default: return GetNewError("Error in DetermineCompatibilityMode: both 'Attributes' and 'Artifact' provided, which is ambiguous") } return nil } func (n *NexusPlugin) IsMultiFileUploadArgsOk(args Args) error { LogPrintln(n, "NexusPlugin IsMultiFileUploadArgsOk") requiredArgs := map[string]string{ "username": args.Username, "password": args.Password, "protocol": args.Protocol, "nexusUrl": args.ServerUrl, "nexusVersion": args.NexusVersion, "repository": args.Repository, "groupId": args.GroupId, "format": args.Format, } for field, value := range requiredArgs { if value == "" { return GetNewError("Error in IsMultiFileUploadArgsOk: " + field + " cannot be empty") } } n.UserName = args.Username n.Password = args.Password n.Repository = args.Repository // Fix Bug #3: Remove trailing slashes from server URL before concatenating serverUrl := strings.TrimRight(args.ServerUrl, "/") n.ServerUrl = args.Protocol + "://" + serverUrl n.GroupId = args.GroupId n.Version = args.NexusVersion n.Format = args.Format // Unmarshalling YAML artifact data var artifacts []Artifact if err := yaml.Unmarshal([]byte(args.Artifact), &artifacts); err != nil { return GetNewError("Error in IsMultiFileUploadArgsOk: Error decoding YAML: " + err.Error()) } var filteredArtifacts []Artifact for _, artifact := range artifacts { missingFields := []string{} if artifact.ArtifactId == "" { missingFields = append(missingFields, "ArtifactId") } if artifact.File == "" { missingFields = append(missingFields, "File") } if artifact.Type == "" { missingFields = append(missingFields, "Type") } if artifact.Version == "" { missingFields = append(missingFields, "Version") } if artifact.GroupId == "" { artifact.GroupId = args.GroupId } if len(missingFields) > 0 { n.addFailedArtifact(artifact, fmt.Sprintf("Missing fields: %s", strings.Join(missingFields, ", "))) } else { // Add to filtered list if all fields are valid filteredArtifacts = append(filteredArtifacts, artifact) } } n.Artifacts = filteredArtifacts return nil } func (n *NexusPlugin) IsSingleFileUploadArgsOk(args Args) error { LogPrintln(n, "NexusPlugin IsSingleFileUploadArgsOk") requiredArgs := map[string]string{ "Username": args.Username, "Password": args.Password, "ServerUrl": args.ServerUrl, "Filename": args.Filename, "Format": args.Format, "Repository": args.Repository, } for field, value := range requiredArgs { if value == "" { return GetNewError("Error in IsSingleFileUploadArgsOk: " + field + " cannot be empty") } } requiredFields := []string{"CgroupId", "Cversion", "Aextension", "Aclassifier"} values := make(map[string]string) pattern := regexp.MustCompile(`-(CgroupId|CartifactId|Cversion|Aextension|Aclassifier)=(\S+)`) matches := pattern.FindAllStringSubmatch(args.Attributes, -1) for _, match := range matches { if len(match) == 3 { values[match[1]] = match[2] } } // Check if all required fields are present for _, field := range requiredFields { if values[field] == "" { return GetNewError("Error in IsSingleFileUploadArgsOk: " + field + " cannot be empty") } } n.UserName = args.Username n.Password = args.Password n.Repository = args.Repository // Fix Bug #3: Remove trailing slashes from server URL n.ServerUrl = strings.TrimRight(args.ServerUrl, "/") n.Format = args.Format n.GroupId = values["CgroupId"] n.Version = "nexus3" n.Artifacts = []Artifact{ { File: args.Filename, Classifier: values["Aclassifier"], ArtifactId: values["CartifactId"], Type: values["Aextension"], Version: values["Cversion"], GroupId: values["CgroupId"], }, } return nil } func (n *NexusPlugin) DoPostArgsValidationSetup(args Args) error { return nil } func (n *NexusPlugin) PersistResults() error { return nil } func (n *NexusPlugin) IsQuiet() bool { return false } func (n *NexusPlugin) InspectProcessArgs(argNamesList []string) (map[string]interface{}, error) { return nil, nil } func GetNewNexusPlugin() NexusPlugin { return NexusPlugin{} } func (n *NexusPlugin) prepareNexus2ArtifactURL(artifact Artifact) string { switch n.Format { case "maven2": return fmt.Sprintf("%s/repository/%s/%s/%s/%s/%s-%s.%s", n.ServerUrl, n.Repository, artifact.GroupId, artifact.ArtifactId, artifact.Version, artifact.ArtifactId, artifact.Version, artifact.Type) case "yum": return fmt.Sprintf("%s/repository/%s/%s/%s", n.ServerUrl, n.Repository, artifact.ArtifactId, artifact.Version) case "raw": return fmt.Sprintf("%s/repository/%s/%s/%s.%s", n.ServerUrl, n.Repository, artifact.GroupId, artifact.ArtifactId, artifact.Type) default: LogPrintln(n, "Unsupported format for direct upload:", n.Format) return "" } } func (n *NexusPlugin) uploadFileNexus2(url string, content io.Reader, filePath string) error { req, err := http.NewRequest("PUT", url, content) if err != nil { return err } req.SetBasicAuth(n.UserName, n.Password) req.Header.Set("Content-Type", "application/octet-stream") resp, err := n.HttpClient.Do(req) if err != nil { return err } defer resp.Body.Close() // Fix Bug #1: Read response body to provide detailed error messages bodyBytes, readErr := io.ReadAll(resp.Body) var bodyContent string if readErr == nil { bodyContent = string(bodyBytes) } if resp.StatusCode >= 400 { fmt.Println("File upload failed status ", resp.StatusCode) if bodyContent != "" { fmt.Println("Response body: ", bodyContent) return fmt.Errorf("Upload failed with status %d: %s", resp.StatusCode, bodyContent) } return fmt.Errorf("Upload failed with status %d", resp.StatusCode) } // Log success response body for debugging if bodyContent != "" { fmt.Println("Upload successful. Response: ", bodyContent) } return nil } func (n *NexusPlugin) uploadFileNexus3(artifact Artifact, filePath string) error { body := &bytes.Buffer{} writer := multipart.NewWriter(body) var url string var assetFieldName string switch n.Format { case "maven2": _ = writer.WriteField("maven2.groupId", artifact.GroupId) _ = writer.WriteField("maven2.artifactId", artifact.ArtifactId) _ = writer.WriteField("maven2.version", artifact.Version) assetFieldName = "maven2.asset1" _ = writer.WriteField("maven2.asset1.extension", artifact.Type) case "raw": _ = writer.WriteField("raw.directory", artifact.GroupId) assetFieldName = "raw.asset1" _ = writer.WriteField("raw.asset1.filename", fmt.Sprintf("%s.%s", artifact.ArtifactId, artifact.Type)) default: assetFieldName = fmt.Sprintf("%s.asset", n.Format) } // Fix Bug #2: Extract basename from file path to avoid sending full paths to Nexus // This handles both Linux (/path/to/file.jar) and Windows (C:\path\to\file.jar) paths basename := filepath.Base(artifact.File) fileWriter, err := writer.CreateFormFile(assetFieldName, basename) if err != nil { LogPrintln(n, "Error CreateFormFile: ", err.Error()) return err } file, err := os.Open(artifact.File) if err != nil { LogPrintln(n, "Error os.Open(artifact.File): ", err.Error()) return err } defer file.Close() _, err = io.Copy(fileWriter, file) if err != nil { LogPrintln(n, "Error io.Copy(fileWriter, file): ", err.Error()) return err } err = writer.Close() if err != nil { LogPrintln(n, "Error writer.Close(): ", err.Error()) return err } url = fmt.Sprintf("%s/service/rest/v1/components?repository=%s", n.ServerUrl, n.Repository) req, err := http.NewRequest("POST", url, body) if err != nil { LogPrintln(n, "Error http.NewRequest: ", err.Error()) return err } req.SetBasicAuth(n.UserName, n.Password) req.Header.Set("Content-Type", writer.FormDataContentType()) resp, err := n.HttpClient.Do(req) if err != nil { LogPrintln(n, "Error n.HttpClient.Do(req): ", err.Error()) return err } defer resp.Body.Close() // Fix Bug #1: Read response body to provide detailed error messages bodyBytes, readErr := io.ReadAll(resp.Body) var bodyContent string if readErr == nil { bodyContent = string(bodyBytes) } if resp.StatusCode >= 400 { LogPrintln(n, "Error upload failed with status: ", resp.StatusCode) if bodyContent != "" { LogPrintln(n, "Response body: ", bodyContent) return fmt.Errorf("Upload failed with status %d: %s", resp.StatusCode, bodyContent) } return fmt.Errorf("Upload failed with status %d", resp.StatusCode) } // Log success response body for debugging if bodyContent != "" { LogPrintln(n, "Upload successful. Response: ", bodyContent) } return nil } func (n *NexusPlugin) addFailedArtifact(artifact Artifact, errMsg string) { n.Failed = append(n.Failed, FailedArtifact{ File: artifact.File, ArtifactId: artifact.ArtifactId, Err: errMsg, }) }