nexus plugin changes

This commit is contained in:
Senthil Kumar T
2024-11-14 14:01:38 +05:30
parent 6fcf0a5f5d
commit 19209aa282
20 changed files with 1351 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
.env
release/
drone-nexus-publish
+60
View File
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="ALL" />
</component>
<component name="ChangeListManager">
<list default="true" id="eaf74dd4-cc6e-4452-911f-10e1bd1a367c" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="GOROOT" url="file://$USER_HOME$/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.7.linux-amd64" />
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 0
}</component>
<component name="ProjectId" id="2oZ333xeDu2iiAyxiIoHbhGAcfW" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.go.formatter.settings.were.checked": "true",
"RunOnceActivity.go.migrated.go.modules.settings": "true",
"RunOnceActivity.go.modules.go.list.on.any.changes.was.set": "true",
"git-widget-placeholder": "drone-nexus-publish-senthil-02",
"go.import.settings.migrated": "true",
"go.sdk.automatically.set": "true",
"last_opened_file_path": "/opt/hns/harness-plugins/nexus/drone-nexus-publish",
"node.js.detected.package.eslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"nodejs_package_manager_path": "npm"
}
}]]></component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-gosdk-2946fb9b3188-155fe4b6e3a0-org.jetbrains.plugins.go.sharedIndexes.bundled-GO-233.15619.13" />
</set>
</attachedChunks>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="VgoProject">
<settings-migrated>true</settings-migrated>
</component>
</project>
+55
View File
@@ -0,0 +1,55 @@
# Blue Oak Model License
Version 1.0.0
## Purpose
This license gives everyone as much permission to work with
this software as possible, while protecting contributors
from liability.
## Acceptance
In order to receive this license, you must agree to its
rules. The rules of this license are both obligations
under that agreement and conditions to your license.
You must not do anything with this software that triggers
a rule that you cannot or will not follow.
## Copyright
Each contributor licenses you to do everything with this
software that would otherwise infringe that contributor's
copyright in it.
## Notices
You must ensure that everyone who gets a copy of
any part of this software from you, with or without
changes, also gets the text of this license or a link to
<https://blueoakcouncil.org/license/1.0.0>.
## Excuse
If anyone notifies you in writing that you have not
complied with [Notices](#notices), you can keep your
license by taking all practical steps to comply within 30
days after the notice. If you do not do so, your license
ends immediately.
## Patent
Each contributor licenses you to do everything with this
software that would otherwise infringe any patent claims
they can license or become able to license.
## Reliability
No contributor can revoke this license.
## No Liability
***As far as the law allows, this software comes as is,
without any warranty or condition, and no contributor
will be liable to anyone for any damages related to this
software or this license, under any kind of legal claim.***
+10
View File
@@ -0,0 +1,10 @@
FROM alpine:3.6 as alpine
RUN apk add -U --no-cache ca-certificates
FROM alpine:3.6
ENV GODEBUG netdns=go
COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ADD release/linux/amd64/drone-nexus-publish /bin/
ENTRYPOINT ["/bin/drone-nexus-publish"]
+10
View File
@@ -0,0 +1,10 @@
FROM alpine:3.6 as alpine
RUN apk add -U --no-cache ca-certificates
FROM alpine:3.6
ENV GODEBUG netdns=go
COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ADD release/linux/arm/plugin /bin/
ENTRYPOINT ["/bin/plugin"]
+10
View File
@@ -0,0 +1,10 @@
FROM alpine:3.6 as alpine
RUN apk add -U --no-cache ca-certificates
FROM alpine:3.6
ENV GODEBUG netdns=go
COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ADD release/linux/arm64/plugin /bin/
ENTRYPOINT ["/bin/plugin"]
+31
View File
@@ -0,0 +1,31 @@
image: lhns/nexus-publish:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
-
image: lhns/nexus-publish:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
-
image: lhns/nexus-publish:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
variant: v8
architecture: arm64
os: linux
-
image: lhns/nexus-publish:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
variant: v7
architecture: arm
os: linux
-
image: lhns/nexus-publish:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
variant: v6
architecture: arm
os: linux
+1
View File
@@ -0,0 +1 @@
UPLOAD_STATUS=Success
+12
View File
@@ -0,0 +1,12 @@
module github.com/harness-community/drone-nexus-publish
go 1.12
require (
github.com/datadrivers/go-nexus-client v1.13.0
github.com/kelseyhightower/envconfig v1.4.0
github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.9.0
gopkg.in/yaml.v2 v2.4.0
)
+39
View File
@@ -0,0 +1,39 @@
github.com/datadrivers/go-nexus-client v1.13.0 h1:8/EMpdfUImdmMLCUf/KGB2883Ryw6pAfAMtjyLQKlGE=
github.com/datadrivers/go-nexus-client v1.13.0/go.mod h1:sPjBOxF7idUoiJoa730L3JyKZodjT0LDAvVF8u4kOLU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+48
View File
@@ -0,0 +1,48 @@
// 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 main
import (
"context"
"github.com/harness-community/drone-nexus-publish/plugin"
plg "github.com/harness-community/drone-nexus-publish/plugin"
"github.com/kelseyhightower/envconfig"
"github.com/sirupsen/logrus"
)
func main() {
logrus.SetFormatter(new(formatter))
var args plg.Args
if err := envconfig.Process("", &args); err != nil {
logrus.Fatalln(err)
}
switch args.Level {
case "debug":
logrus.SetFormatter(textFormatter)
logrus.SetLevel(logrus.DebugLevel)
case "trace":
logrus.SetFormatter(textFormatter)
logrus.SetLevel(logrus.TraceLevel)
}
if _, err := plugin.Exec(context.Background(), args); err != nil {
logrus.Fatalln(err)
}
}
// default formatter that writes logs without including timestamp
// or level information.
type formatter struct{}
func (*formatter) Format(entry *logrus.Entry) ([]byte, error) {
return []byte(entry.Message), nil
}
// text formatter that writes logs with level information
var textFormatter = &logrus.TextFormatter{
DisableTimestamp: true,
}
+45
View File
@@ -0,0 +1,45 @@
package plugin
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 {
Pipeline
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"`
}
+445
View File
@@ -0,0 +1,445 @@
package plugin
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"regexp"
"strings"
"gopkg.in/yaml.v2"
)
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{}
}
for _, 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
}
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())
}
fmt.Println("Successfully uploaded artifact:", filePath)
}
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
n.ServerUrl = args.Protocol + "://" + args.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
n.ServerUrl = 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()
if resp.StatusCode >= 400 {
fmt.Println("File upload failed status ", resp.StatusCode)
return fmt.Errorf("Upload failed with status %d", resp.StatusCode)
}
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)
}
fileWriter, err := writer.CreateFormFile(assetFieldName, artifact.File)
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()
if resp.StatusCode >= 400 {
LogPrintln(n, "Error upload failed with status: ", resp.StatusCode)
return fmt.Errorf("Upload failed with status %d", resp.StatusCode)
}
return nil
}
func (n *NexusPlugin) addFailedArtifact(artifact Artifact, errMsg string) {
n.Failed = append(n.Failed, FailedArtifact{
File: artifact.File,
ArtifactId: artifact.ArtifactId,
Err: errMsg,
})
}
+185
View File
@@ -0,0 +1,185 @@
package plugin
import (
"io/ioutil"
"net/http"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type MockHttpClient struct {
mock.Mock
}
func (m *MockHttpClient) Do(req *http.Request) (*http.Response, error) {
args := m.Called(req)
return args.Get(0).(*http.Response), args.Error(1)
}
// Utility function to create a temporary file for testing
func createTempFile(content string) (string, error) {
tmpFile, err := ioutil.TempFile("", "testfile_*.zip")
if err != nil {
return "", err
}
if _, err := tmpFile.Write([]byte(content)); err != nil {
tmpFile.Close()
return "", err
}
if err := tmpFile.Close(); err != nil {
return "", err
}
return tmpFile.Name(), nil
}
func TestNexusPlugin_Run_UploadFailed(t *testing.T) {
mockClient := new(MockHttpClient)
mockResp := &http.Response{
StatusCode: 500,
Body: ioutil.NopCloser(strings.NewReader("Internal Server Error")),
}
mockClient.On("Do", mock.AnythingOfType("*http.Request")).Return(mockResp, nil)
tmpFile, err := createTempFile("testfile.zip")
assert.NoError(t, err)
defer os.Remove(tmpFile)
plugin := NexusPlugin{
PluginProcessingInfo: PluginProcessingInfo{
UserName: "testUser",
Password: "testPass",
ServerUrl: "https://nexus.example.com",
Repository: "repo",
GroupId: "group",
Version: "1.0.0",
Artifacts: []Artifact{
{
File: tmpFile,
ArtifactId: "artifact123",
Type: "zip",
},
},
},
HttpClient: mockClient,
}
err = plugin.Run()
assert.NotNil(t, err)
assert.Len(t, plugin.Failed, 1)
assert.Equal(t, tmpFile, plugin.Failed[0].File)
assert.Equal(t, "artifact123", plugin.Failed[0].ArtifactId)
assert.Contains(t, plugin.Failed[0].Err, "upload failed")
mockClient.AssertExpectations(t)
}
// The following tests validate argument processing without needing an actual file
func TestNexusPlugin_ValidateAndProcessArgs_MultiFileUpload_Success(t *testing.T) {
args := Args{
EnvPluginInputArgs: EnvPluginInputArgs{
Username: "testUser",
Password: "testPass",
Protocol: "https",
ServerUrl: "nexus.example.com",
NexusVersion: "3",
Repository: "repo",
GroupId: "group",
Format: "maven2",
Artifact: "[{ \"artifactId\": \"artifact123\", \"file\": \"testfile.zip\", \"type\": \"zip\", \"version\": \"1\" }]",
},
}
plugin := NexusPlugin{}
err := plugin.ValidateAndProcessArgs(args)
assert.Nil(t, err)
assert.Len(t, plugin.Artifacts, 1)
assert.Equal(t, "testfile.zip", plugin.Artifacts[0].File)
assert.Equal(t, "artifact123", plugin.Artifacts[0].ArtifactId)
}
func TestNexusPlugin_ValidateAndProcessArgs_SingleFileUpload_Success(t *testing.T) {
args := Args{
EnvPluginInputArgs: EnvPluginInputArgs{
Username: "testUser",
Password: "testPass",
ServerUrl: "https://nexus.example.com",
Filename: "testfile.zip",
Format: "zip",
Repository: "repo",
Attributes: "-CgroupId=group -CartifactId=artifact123 -Cversion=1.0.0 -Aextension=zip -Aclassifier=classifier",
},
}
plugin := NexusPlugin{}
err := plugin.ValidateAndProcessArgs(args)
assert.Nil(t, err)
assert.Len(t, plugin.Artifacts, 1)
assert.Equal(t, "testfile.zip", plugin.Artifacts[0].File)
assert.Equal(t, "artifact123", plugin.Artifacts[0].ArtifactId)
}
func TestNexusPlugin_ValidateAndProcessArgs_MissingArguments(t *testing.T) {
args := Args{
EnvPluginInputArgs: EnvPluginInputArgs{
Username: "testUser",
Password: "testPass",
ServerUrl: "https://nexus.example.com",
Filename: "testfile.zip",
Format: "zip",
Repository: "repo",
},
}
plugin := NexusPlugin{}
err := plugin.ValidateAndProcessArgs(args)
assert.NotNil(t, err)
assert.Equal(t, "Error in DetermineCompatibilityMode: both 'Attributes' and 'Artifact' cannot be empty", err.Error())
}
func TestNexusPlugin_Run_MultiFileUpload_Success(t *testing.T) {
mockClient := new(MockHttpClient)
mockResp := &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(strings.NewReader("Success")),
}
mockClient.On("Do", mock.AnythingOfType("*http.Request")).Return(mockResp, nil).Maybe()
tmpFile1, err := createTempFile("file1.zip")
assert.NoError(t, err)
defer os.Remove(tmpFile1)
tmpFile2, err := createTempFile("file2.zip")
assert.NoError(t, err)
defer os.Remove(tmpFile2)
plugin := NexusPlugin{
PluginProcessingInfo: PluginProcessingInfo{
UserName: "testUser",
Password: "testPass",
ServerUrl: "https://nexus.example.com",
Repository: "repo",
GroupId: "group",
Version: "1.0.0",
Format: "maven2",
Artifacts: []Artifact{
{File: tmpFile1, ArtifactId: "artifact1", Type: "zip", Version: "1"},
{File: tmpFile2, ArtifactId: "artifact2", Type: "zip", Version: "1"},
},
},
HttpClient: mockClient,
}
err = plugin.Run()
assert.Nil(t, err)
assert.Empty(t, plugin.Failed)
mockClient.AssertExpectations(t)
}
+149
View File
@@ -0,0 +1,149 @@
// 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
// Pipeline provides Pipeline metadata from the environment.
type Pipeline struct {
// Build provides build metadata.
Build struct {
Branch string `envconfig:"DRONE_BUILD_BRANCH"`
Number int `envconfig:"DRONE_BUILD_NUMBER"`
Parent int `envconfig:"DRONE_BUILD_PARENT"`
Event string `envconfig:"DRONE_BUILD_EVENT"`
Action string `envconfig:"DRONE_BUILD_ACTION"`
Status string `envconfig:"DRONE_BUILD_STATUS"`
Created int64 `envconfig:"DRONE_BUILD_CREATED"`
Started int64 `envconfig:"DRONE_BUILD_STARTED"`
Finished int64 `envconfig:"DRONE_BUILD_FINISHED"`
Link string `envconfig:"DRONE_BUILD_LINK"`
}
// Calver provides the calver details parsed from the
// git tag. If the git tag is empty or is not a valid
// calver, the values will be empty.
Calver struct {
Version string `envconfig:"DRONE_CALVER"`
Short string `envconfig:"DRONE_CALVER_SHORT"`
MajorMinor string `envconfig:"DRONE_CALVER_MAJOR_MINOR"`
Major string `envconfig:"DRONE_CALVER_MAJOR"`
Minor string `envconfig:"DRONE_CALVER_MINOR"`
Micro string `envconfig:"DRONE_CALVER_MICRO"`
Modifier string `envconfig:"DRONE_CALVER_MODIFIER"`
}
// Card provides adaptive card configuration options.
Card struct {
Path string `envconfig:"DRONE_CARD_PATH"`
}
// Commit provides the commit metadata.
Commit struct {
Rev string `envconfig:"DRONE_COMMIT_SHA"`
Before string `envconfig:"DRONE_COMMIT_BEFORE"`
After string `envconfig:"DRONE_COMMIT_AFTER"`
Ref string `envconfig:"DRONE_COMMIT_REF"`
Branch string `envconfig:"DRONE_COMMIT_BRANCH"`
Source string `envconfig:"DRONE_COMMIT_SOURCE"`
Target string `envconfig:"DRONE_COMMIT_TARGET"`
Link string `envconfig:"DRONE_COMMIT_LINK"`
Message string `envconfig:"DRONE_COMMIT_MESSAGE"`
Author struct {
Username string `envconfig:"DRONE_COMMIT_AUTHOR"`
Name string `envconfig:"DRONE_COMMIT_AUTHOR_NAME"`
Email string `envconfig:"DRONE_COMMIT_AUTHOR_EMAIL"`
Avatar string `envconfig:"DRONE_COMMIT_AUTHOR_AVATAR"`
}
}
// Deploy provides the deployment metadata.
Deploy struct {
ID string `envconfig:"DRONE_DEPLOY_TO"`
Target string `envconfig:"DRONE_DEPLOY_ID"`
}
// Failed provides a list of failed steps and failed stages
// for the current pipeline.
Failed struct {
Steps []string `envconfig:"DRONE_FAILED_STEPS"`
Stages []string `envconfig:"DRONE_FAILED_STAGES"`
}
// Git provides the git repository metadata.
Git struct {
HTTPURL string `envconfig:"DRONE_GIT_HTTP_URL"`
SSHURL string `envconfig:"DRONE_GIT_SSH_URL"`
}
// PullRequest provides the pull request metadata.
PullRequest struct {
Number int `envconfig:"DRONE_PULL_REQUEST"`
}
// Repo provides the repository metadata.
Repo struct {
Branch string `envconfig:"DRONE_REPO_BRANCH"`
Link string `envconfig:"DRONE_REPO_LINK"`
Namespace string `envconfig:"DRONE_REPO_NAMESPACE"`
Name string `envconfig:"DRONE_REPO_NAME"`
Private bool `envconfig:"DRONE_REPO_PRIVATE"`
Remote string `envconfig:"DRONE_GIT_HTTP_URL"`
SCM string `envconfig:"DRONE_REPO_SCM"`
Slug string `envconfig:"DRONE_REPO"`
Visibility string `envconfig:"DRONE_REPO_VISIBILITY"`
}
// Stage provides the stage metadata.
Stage struct {
Kind string `envconfig:"DRONE_STAGE_KIND"`
Type string `envconfig:"DRONE_STAGE_TYPE"`
Name string `envconfig:"DRONE_STAGE_NAME"`
Number int `envconfig:"DRONE_STAGE_NUMBER"`
Machine string `envconfig:"DRONE_STAGE_MACHINE"`
OS string `envconfig:"DRONE_STAGE_OS"`
Arch string `envconfig:"DRONE_STAGE_ARCH"`
Variant string `envconfig:"DRONE_STAGE_VARIANT"`
Status string `envconfig:"DRONE_STAGE_STATUS"`
Started int64 `envconfig:"DRONE_STAGE_STARTED"`
Finished int64 `envconfig:"DRONE_STAGE_FINISHED"`
DependsOn []string `envconfig:"DRONE_STAGE_DEPENDS_ON"`
}
// Step provides the step metadata.
Step struct {
Number int `envconfig:"DRONE_STEP_NUMBER"`
Name string `envconfig:"DRONE_STEP_NAME"`
}
// Semver provides the semver details parsed from the
// git tag. If the git tag is empty or is not a valid
// semver, the values will be empty and the error field
// will be populated with the parsing error.
Semver struct {
Version string `envconfig:"DRONE_SEMVER"`
Short string `envconfig:"DRONE_SEMVER_SHORT"`
Major string `envconfig:"DRONE_SEMVER_MAJOR"`
Minor string `envconfig:"DRONE_SEMVER_MINOR"`
Patch string `envconfig:"DRONE_SEMVER_PATCH"`
Build string `envconfig:"DRONE_SEMVER_BUILD"`
PreRelease string `envconfig:"DRONE_SEMVER_PRERELEASE"`
Error string `envconfig:"DRONE_SEMVER_ERROR"`
}
// System provides the Drone system metadata, including
// the system version of details required to create the
// drone website address.
System struct {
Proto string `envconfig:"DRONE_SYSTEM_PROTO"`
Host string `envconfig:"DRONE_SYSTEM_HOST"`
Hostname string `envconfig:"DRONE_SYSTEM_HOSTNAME"`
Version string `envconfig:"DRONE_SYSTEM_VERSION"`
}
// Tag provides the git tag details.
Tag struct {
Name string `envconfig:"DRONE_TAG"`
}
}
+62
View File
@@ -0,0 +1,62 @@
// 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 (
"context"
)
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
}
+11
View File
@@ -0,0 +1,11 @@
// 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 "testing"
func TestPlugin(t *testing.T) {
t.Skip()
}
+154
View File
@@ -0,0 +1,154 @@
// 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 (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"reflect"
"github.com/sirupsen/logrus"
)
func GetNewError(s string) error {
return errors.New(s)
}
func LogPrintln(p Plugin, args ...interface{}) {
if !IsDevTestingMode() {
return
}
if p != nil {
if p.IsQuiet() {
return
}
}
logrus.Println(append([]interface{}{"Plugin Info:"}, args...)...)
}
func LogPrintf(p Plugin, format string, v ...interface{}) {
if !IsDevTestingMode() {
return
}
if p != nil {
if p.IsQuiet() {
return
}
}
logrus.Printf(format, v...)
}
func IsDirExists(dir string) (bool, error) {
info, err := os.Stat(dir)
if os.IsNotExist(err) {
return false, err
}
return info.IsDir(), nil
}
func CreateDir(absolutePath string) error {
if absolutePath == "" || absolutePath == "." || absolutePath == ".." {
return nil
}
err := os.MkdirAll(absolutePath, os.ModePerm)
if err != nil {
return fmt.Errorf("failed to create directory %s: %w", absolutePath, err)
}
return nil
}
func GetOutputVariablesStorageFilePath() string {
if IsDevTestingMode() {
return filepath.Join("/tmp", "drone-output")
}
return os.Getenv("DRONE_OUTPUT")
}
func ReadFileAsString(filePath string) (string, error) {
data, err := os.ReadFile(filePath)
if err != nil {
return "", err
}
return string(data), nil
}
func WriteEnvVariableAsString(key string, value interface{}) error {
if GetOutputVariablesStorageFilePath() == "" {
return GetNewError("Output file path is empty, check env var DRONE_OUTPUT")
}
outputFile, err := os.OpenFile(GetOutputVariablesStorageFilePath(), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("failed to open output file: %w", err)
}
defer outputFile.Close()
valueStr := fmt.Sprintf("%v", value)
_, err = fmt.Fprintf(outputFile, "%s=%s\n", key, valueStr)
if err != nil {
return fmt.Errorf("failed to write to env: %w", err)
}
return nil
}
func IsDevTestingMode() bool {
return os.Getenv("DEV_TEST_d6c9b463090c") == "true"
}
func StructToJSONWithEnvKeys(v interface{}) (string, error) {
val := reflect.ValueOf(v)
typ := reflect.TypeOf(v)
data := make(map[string]interface{})
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
key := field.Tag.Get("envconfig")
if key != "" {
data[key] = val.Field(i).Interface()
}
}
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
return "", err
}
return string(jsonData), nil
}
func GetTestWorkSpaceDir() string {
nexusWorkSpaceDir := os.Getenv(DefaultWorkSpaceDirEnvVarKey)
if nexusWorkSpaceDir == "" {
nexusWorkSpaceDir = TestWorkSpaceDir
}
return nexusWorkSpaceDir
}
func GetTestBuildRootDir() string {
return GetTestWorkSpaceDir()
}
const (
DefaultWorkSpaceDirEnvVarKey = "DRONE_WORKSPACE"
TestWorkSpaceDir = "../test/tmp_workspace"
)
//
//
+18
View File
@@ -0,0 +1,18 @@
#!/bin/sh
# force go modules
export GOPATH=""
# disable cgo
export CGO_ENABLED=0
set -e
set -x
# linux
GOOS=linux GOARCH=amd64 go build -o release/linux/amd64/drone-nexus-publish
#GOOS=linux GOARCH=arm64 go build -o release/linux/arm64/drone-nexus-publish
#GOOS=linux GOARCH=arm go build -o release/linux/arm/drone-nexus-publish
# windows
#GOOS=windows go build -o release/windows/amd64/drone-nexus-publish.exe
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
curl https://raw.githubusercontent.com/drone/boilr-plugin/master/template/plugin/pipeline.go --output plugin/pipeline.go