1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-01-04 04:07:16 +02:00

Implement nexusUpload command and tests (#1255)

Artifacts to upload are assembled for MTA projects and Maven projects with optional application sub-module. Then maven deploy:deploy-file is used as backend to upload bundles of artifacts plus sub-artifacts.

Co-authored-by: Florian Wilhelm <florian.wilhelm02@sap.com>
This commit is contained in:
Stephan Aßmus 2020-03-20 18:20:52 +01:00 committed by GitHub
parent b39080409c
commit 20b65d5a2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1982 additions and 408 deletions

464
cmd/nexusUpload.go Normal file
View File

@ -0,0 +1,464 @@
package cmd
import (
"encoding/json"
"fmt"
"github.com/pkg/errors"
"os"
"path/filepath"
"github.com/SAP/jenkins-library/pkg/command"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/maven"
"github.com/SAP/jenkins-library/pkg/nexus"
"github.com/SAP/jenkins-library/pkg/piperenv"
"github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/SAP/jenkins-library/pkg/telemetry"
"github.com/ghodss/yaml"
)
// nexusUploadUtils defines an interface for utility functionality used from external packages,
// so it can be easily mocked for testing.
type nexusUploadUtils interface {
fileExists(path string) (bool, error)
fileRead(path string) ([]byte, error)
fileWrite(path string, content []byte, perm os.FileMode) error
fileRemove(path string)
usesMta() bool
usesMaven() bool
getEnvParameter(path, name string) string
getExecRunner() execRunner
evaluate(pomFile, expression string) (string, error)
}
type utilsBundle struct {
projectStructure piperutils.ProjectStructure
fileUtils piperutils.Files
execRunner *command.Command
}
func newUtilsBundle() *utilsBundle {
return &utilsBundle{
projectStructure: piperutils.ProjectStructure{},
fileUtils: piperutils.Files{},
}
}
func (u *utilsBundle) fileExists(path string) (bool, error) {
return u.fileUtils.FileExists(path)
}
func (u *utilsBundle) fileRead(path string) ([]byte, error) {
return u.fileUtils.FileRead(path)
}
func (u *utilsBundle) fileWrite(filePath string, content []byte, perm os.FileMode) error {
parent := filepath.Dir(filePath)
if parent != "" {
err := u.fileUtils.MkdirAll(parent, 0775)
if err != nil {
return err
}
}
return u.fileUtils.FileWrite(filePath, content, perm)
}
func (u *utilsBundle) fileRemove(path string) {
err := os.Remove(path)
if err != nil {
log.Entry().WithError(err).Warnf("Failed to remove file '%s'.", path)
}
}
func (u *utilsBundle) usesMta() bool {
return u.projectStructure.UsesMta()
}
func (u *utilsBundle) usesMaven() bool {
return u.projectStructure.UsesMaven()
}
func (u *utilsBundle) getEnvParameter(path, name string) string {
return piperenv.GetParameter(path, name)
}
func (u *utilsBundle) getExecRunner() execRunner {
if u.execRunner == nil {
u.execRunner = &command.Command{}
u.execRunner.Stdout(log.Entry().Writer())
u.execRunner.Stderr(log.Entry().Writer())
}
return u.execRunner
}
func (u *utilsBundle) evaluate(pomFile, expression string) (string, error) {
return maven.Evaluate(pomFile, expression, u.getExecRunner())
}
func nexusUpload(options nexusUploadOptions, _ *telemetry.CustomData) {
utils := newUtilsBundle()
uploader := nexus.Upload{}
err := runNexusUpload(utils, &uploader, &options)
if err != nil {
log.Entry().WithError(err).Fatal("step execution failed")
}
}
func runNexusUpload(utils nexusUploadUtils, uploader nexus.Uploader, options *nexusUploadOptions) error {
err := uploader.SetRepoURL(options.Url, options.Version, options.Repository)
if err != nil {
return err
}
if utils.usesMta() {
log.Entry().Info("MTA project structure detected")
return uploadMTA(utils, uploader, options)
} else if utils.usesMaven() {
log.Entry().Info("Maven project structure detected")
return uploadMaven(utils, uploader, options)
} else {
return fmt.Errorf("unsupported project structure")
}
}
func uploadMTA(utils nexusUploadUtils, uploader nexus.Uploader, options *nexusUploadOptions) error {
if options.GroupID == "" {
return fmt.Errorf("the 'groupId' parameter needs to be provided for MTA projects")
}
var mtaPath string
exists, _ := utils.fileExists("mta.yaml")
if exists {
mtaPath = "mta.yaml"
// Give this file precedence, but it would be even better if
// ProjectStructure could be asked for the mta file it detected.
} else {
// This will fail anyway if the file doesn't exist
mtaPath = "mta.yml"
}
version, err := getVersionFromMtaFile(utils, mtaPath)
var artifactID = options.ArtifactID
if artifactID == "" {
artifactID = utils.getEnvParameter(".pipeline/commonPipelineEnvironment/configuration", "artifactId")
if artifactID == "" {
err = fmt.Errorf("the 'artifactId' parameter was not provided and could not be retrieved from the Common Pipeline Environment")
} else {
log.Entry().Debugf("mtar artifact id from CPE: '%s'", artifactID)
}
}
if err == nil {
err = uploader.SetInfo(options.GroupID, artifactID, version)
if err == nexus.ErrEmptyVersion {
err = fmt.Errorf("the project descriptor file 'mta.yaml' has an invalid version: %w", err)
}
}
if err == nil {
err = addArtifact(utils, uploader, mtaPath, "", "yaml")
}
if err == nil {
mtarFilePath := utils.getEnvParameter(".pipeline/commonPipelineEnvironment", "mtarFilePath")
log.Entry().Debugf("mtar file path: '%s'", mtarFilePath)
err = addArtifact(utils, uploader, mtarFilePath, "", "mtar")
}
if err == nil {
err = uploadArtifacts(utils, uploader, options, false)
}
return err
}
type mtaYaml struct {
ID string `json:"ID"`
Version string `json:"version"`
}
func getVersionFromMtaFile(utils nexusUploadUtils, filePath string) (string, error) {
mtaYamlContent, err := utils.fileRead(filePath)
if err != nil {
return "", fmt.Errorf("could not read from required project descriptor file '%s'",
filePath)
}
return getVersionFromMtaYaml(mtaYamlContent, filePath)
}
func getVersionFromMtaYaml(mtaYamlContent []byte, filePath string) (string, error) {
var mtaYaml mtaYaml
err := yaml.Unmarshal(mtaYamlContent, &mtaYaml)
if err != nil {
// Eat the original error as it is unhelpful and confusingly mentions JSON, while the
// user thinks it should parse YAML (it is transposed by the implementation).
return "", fmt.Errorf("failed to parse contents of the project descriptor file '%s'",
filePath)
}
return mtaYaml.Version, nil
}
func createMavenExecuteOptions(options *nexusUploadOptions) maven.ExecuteOptions {
mavenOptions := maven.ExecuteOptions{
ReturnStdout: false,
M2Path: options.M2Path,
GlobalSettingsFile: options.GlobalSettingsFile,
}
return mavenOptions
}
const settingsServerID = "artifact.deployment.nexus"
const nexusMavenSettings = `<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>artifact.deployment.nexus</id>
<username>${env.NEXUS_username}</username>
<password>${env.NEXUS_password}</password>
</server>
</servers>
</settings>
`
const settingsPath = ".pipeline/nexusMavenSettings.xml"
func setupNexusCredentialsSettingsFile(utils nexusUploadUtils, options *nexusUploadOptions,
mavenOptions *maven.ExecuteOptions) (string, error) {
if options.User == "" || options.Password == "" {
return "", nil
}
err := utils.fileWrite(settingsPath, []byte(nexusMavenSettings), os.ModePerm)
if err != nil {
return "", fmt.Errorf("failed to write maven settings file to '%s': %w", settingsPath, err)
}
log.Entry().Debugf("Writing nexus credentials to environment")
utils.getExecRunner().SetEnv([]string{"NEXUS_username=" + options.User, "NEXUS_password=" + options.Password})
mavenOptions.ProjectSettingsFile = settingsPath
mavenOptions.Defines = append(mavenOptions.Defines, "-DrepositoryId="+settingsServerID)
return settingsPath, nil
}
type artifactDefines struct {
file string
packaging string
files string
classifiers string
types string
}
const deployGoal = "org.apache.maven.plugins:maven-deploy-plugin:2.7:deploy-file"
func uploadArtifacts(utils nexusUploadUtils, uploader nexus.Uploader, options *nexusUploadOptions,
generatePOM bool) error {
if uploader.GetGroupID() == "" {
return fmt.Errorf("no group ID was provided, or could be established from project files")
}
artifacts := uploader.GetArtifacts()
if len(artifacts) == 0 {
return errors.New("no artifacts to upload")
}
var defines []string
defines = append(defines, "-Durl=http://"+uploader.GetRepoURL())
defines = append(defines, "-DgroupId="+uploader.GetGroupID())
defines = append(defines, "-Dversion="+uploader.GetArtifactsVersion())
defines = append(defines, "-DartifactId="+uploader.GetArtifactsID())
mavenOptions := createMavenExecuteOptions(options)
mavenOptions.Goals = []string{deployGoal}
mavenOptions.Defines = defines
settingsFile, err := setupNexusCredentialsSettingsFile(utils, options, &mavenOptions)
if err != nil {
return fmt.Errorf("writing credential settings for maven failed: %w", err)
}
if settingsFile != "" {
defer utils.fileRemove(settingsFile)
}
// iterate over the artifact descriptions, the first one is the main artifact, the following ones are
// sub-artifacts.
var d artifactDefines
for i, artifact := range artifacts {
if i == 0 {
d.file = artifact.File
d.packaging = artifact.Type
} else {
// Note: It is important to append the comma, even when the list is empty
// or the appended item is empty. So classifiers could end up like ",,classes".
// This is needed to match the third classifier "classes" to the third sub-artifact.
d.files = appendItemToString(d.files, artifact.File, i == 1)
d.classifiers = appendItemToString(d.classifiers, artifact.Classifier, i == 1)
d.types = appendItemToString(d.types, artifact.Type, i == 1)
}
}
err = uploadArtifactsBundle(d, generatePOM, mavenOptions, utils.getExecRunner())
if err != nil {
return fmt.Errorf("uploading artifacts for ID '%s' failed: %w", uploader.GetArtifactsID(), err)
}
uploader.Clear()
return nil
}
// appendItemToString appends a comma this is not the first item, regardless of whether
// list or item are empty.
func appendItemToString(list, item string, first bool) string {
if !first {
list += ","
}
return list + item
}
func uploadArtifactsBundle(d artifactDefines, generatePOM bool, mavenOptions maven.ExecuteOptions,
execRunner execRunner) error {
if d.file == "" {
return fmt.Errorf("no file specified")
}
var defines []string
defines = append(defines, "-Dfile="+d.file)
defines = append(defines, "-Dpackaging="+d.packaging)
if !generatePOM {
defines = append(defines, "-DgeneratePom=false")
}
if len(d.files) > 0 {
defines = append(defines, "-Dfiles="+d.files)
defines = append(defines, "-Dclassifiers="+d.classifiers)
defines = append(defines, "-Dtypes="+d.types)
}
mavenOptions.Defines = append(mavenOptions.Defines, defines...)
_, err := maven.Execute(&mavenOptions, execRunner)
return err
}
func addArtifact(utils nexusUploadUtils, uploader nexus.Uploader, filePath, classifier, fileType string) error {
exists, _ := utils.fileExists(filePath)
if !exists {
return fmt.Errorf("artifact file not found '%s'", filePath)
}
artifact := nexus.ArtifactDescription{
File: filePath,
Type: fileType,
Classifier: classifier,
}
return uploader.AddArtifact(artifact)
}
var errPomNotFound = errors.New("pom.xml not found")
func uploadMaven(utils nexusUploadUtils, uploader nexus.Uploader, options *nexusUploadOptions) error {
err := uploadMavenArtifacts(utils, uploader, options, "", "target", "")
if err != nil {
return err
}
// Test if a sub-folder "application" exists and upload the artifacts from there as well.
// This means there are built-in assumptions about the project structure (archetype),
// that nexusUpload supports. To make this more flexible should be the scope of another PR.
err = uploadMavenArtifacts(utils, uploader, options, "application", "application/target",
options.AdditionalClassifiers)
if err == errPomNotFound {
// Ignore for missing application module
err = nil
}
return err
}
func uploadMavenArtifacts(utils nexusUploadUtils, uploader nexus.Uploader, options *nexusUploadOptions,
pomPath, targetFolder, additionalClassifiers string) error {
pomFile := composeFilePath(pomPath, "pom", "xml")
exists, _ := utils.fileExists(pomFile)
if !exists {
return errPomNotFound
}
groupID, _ := utils.evaluate(pomFile, "project.groupId")
if groupID == "" {
groupID = options.GroupID
}
artifactID, err := utils.evaluate(pomFile, "project.artifactId")
var artifactsVersion string
if err == nil {
artifactsVersion, err = utils.evaluate(pomFile, "project.version")
}
if err == nil {
err = uploader.SetInfo(groupID, artifactID, artifactsVersion)
}
var finalBuildName string
if err == nil {
finalBuildName, _ = utils.evaluate(pomFile, "project.build.finalName")
if finalBuildName == "" {
// Fallback to using artifactID as base-name of artifact files.
finalBuildName = artifactID
}
}
if err == nil {
err = addArtifact(utils, uploader, pomFile, "", "pom")
}
if err == nil {
err = addMavenTargetArtifact(utils, uploader, pomFile, targetFolder, finalBuildName)
}
if err == nil {
err = addMavenTargetSubArtifacts(utils, uploader, additionalClassifiers, targetFolder, finalBuildName)
}
if err == nil {
err = uploadArtifacts(utils, uploader, options, true)
}
return err
}
func addMavenTargetArtifact(utils nexusUploadUtils, uploader nexus.Uploader,
pomFile, targetFolder, finalBuildName string) error {
packaging, err := utils.evaluate(pomFile, "project.packaging")
if err != nil {
return err
}
if packaging == "pom" {
// Only pom.xml itself is the artifact
return nil
}
if packaging == "" {
packaging = "jar"
}
filePath := composeFilePath(targetFolder, finalBuildName, packaging)
return addArtifact(utils, uploader, filePath, "", packaging)
}
func addMavenTargetSubArtifacts(utils nexusUploadUtils, uploader nexus.Uploader,
additionalClassifiers, targetFolder, finalBuildName string) error {
if additionalClassifiers == "" {
return nil
}
classifiers, err := getClassifiers(additionalClassifiers)
if err != nil {
return err
}
for _, classifier := range classifiers {
if classifier.Classifier == "" || classifier.FileType == "" {
return fmt.Errorf("invalid additional classifier description (classifier: '%s', type: '%s')",
classifier.Classifier, classifier.FileType)
}
filePath := composeFilePath(targetFolder, finalBuildName+"-"+classifier.Classifier, classifier.FileType)
err = addArtifact(utils, uploader, filePath, classifier.Classifier, classifier.FileType)
if err != nil {
return err
}
}
return nil
}
func composeFilePath(folder, name, extension string) string {
fileName := name + "." + extension
return filepath.Join(folder, fileName)
}
type classifierDescription struct {
Classifier string `json:"classifier"`
FileType string `json:"type"`
}
func getClassifiers(classifiersAsJSON string) ([]classifierDescription, error) {
var classifiers []classifierDescription
err := json.Unmarshal([]byte(classifiersAsJSON), &classifiers)
return classifiers, err
}

View File

@ -0,0 +1,175 @@
// Code generated by piper's step-generator. DO NOT EDIT.
package cmd
import (
"fmt"
"os"
"time"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/telemetry"
"github.com/spf13/cobra"
)
type nexusUploadOptions struct {
Version string `json:"version,omitempty"`
Url string `json:"url,omitempty"`
Repository string `json:"repository,omitempty"`
GroupID string `json:"groupId,omitempty"`
ArtifactID string `json:"artifactId,omitempty"`
GlobalSettingsFile string `json:"globalSettingsFile,omitempty"`
M2Path string `json:"m2Path,omitempty"`
AdditionalClassifiers string `json:"additionalClassifiers,omitempty"`
User string `json:"user,omitempty"`
Password string `json:"password,omitempty"`
}
// NexusUploadCommand Upload artifacts to Nexus
func NexusUploadCommand() *cobra.Command {
metadata := nexusUploadMetadata()
var stepConfig nexusUploadOptions
var startTime time.Time
var createNexusUploadCmd = &cobra.Command{
Use: "nexusUpload",
Short: "Upload artifacts to Nexus",
Long: `Upload build artifacts to a Nexus Repository Manager`,
PreRunE: func(cmd *cobra.Command, args []string) error {
startTime = time.Now()
log.SetStepName("nexusUpload")
log.SetVerbose(GeneralConfig.Verbose)
return PrepareConfig(cmd, &metadata, "nexusUpload", &stepConfig, config.OpenPiperFile)
},
Run: func(cmd *cobra.Command, args []string) {
telemetryData := telemetry.CustomData{}
telemetryData.ErrorCode = "1"
handler := func() {
telemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds())
telemetry.Send(&telemetryData)
}
log.DeferExitHandler(handler)
defer handler()
telemetry.Initialize(GeneralConfig.NoTelemetry, "nexusUpload")
nexusUpload(stepConfig, &telemetryData)
telemetryData.ErrorCode = "0"
},
}
addNexusUploadFlags(createNexusUploadCmd, &stepConfig)
return createNexusUploadCmd
}
func addNexusUploadFlags(cmd *cobra.Command, stepConfig *nexusUploadOptions) {
cmd.Flags().StringVar(&stepConfig.Version, "version", "nexus3", "The Nexus Repository Manager version. Currently supported are 'nexus2' and 'nexus3'.")
cmd.Flags().StringVar(&stepConfig.Url, "url", os.Getenv("PIPER_url"), "URL of the nexus. The scheme part of the URL will not be considered, because only http is supported.")
cmd.Flags().StringVar(&stepConfig.Repository, "repository", os.Getenv("PIPER_repository"), "Name of the nexus repository.")
cmd.Flags().StringVar(&stepConfig.GroupID, "groupId", os.Getenv("PIPER_groupId"), "Group ID of the artifacts. Only used in MTA projects, ignored for Maven.")
cmd.Flags().StringVar(&stepConfig.ArtifactID, "artifactId", os.Getenv("PIPER_artifactId"), "The artifact ID used for both the .mtar and mta.yaml files deployed for MTA projects, ignored for Maven.")
cmd.Flags().StringVar(&stepConfig.GlobalSettingsFile, "globalSettingsFile", os.Getenv("PIPER_globalSettingsFile"), "Path to the mvn settings file that should be used as global settings file.")
cmd.Flags().StringVar(&stepConfig.M2Path, "m2Path", os.Getenv("PIPER_m2Path"), "The path to the local .m2 directory, only used for Maven projects.")
cmd.Flags().StringVar(&stepConfig.AdditionalClassifiers, "additionalClassifiers", os.Getenv("PIPER_additionalClassifiers"), "List of additional classifiers that should be deployed to nexus. Each item is a map of a type and a classifier name.")
cmd.Flags().StringVar(&stepConfig.User, "user", os.Getenv("PIPER_user"), "User")
cmd.Flags().StringVar(&stepConfig.Password, "password", os.Getenv("PIPER_password"), "Password")
cmd.MarkFlagRequired("url")
cmd.MarkFlagRequired("repository")
}
// retrieve step metadata
func nexusUploadMetadata() config.StepData {
var theMetaData = config.StepData{
Metadata: config.StepMetadata{
Name: "nexusUpload",
Aliases: []config.Alias{{Name: "mavenExecute", Deprecated: false}},
},
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{
Name: "version",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "url",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: true,
Aliases: []config.Alias{},
},
{
Name: "repository",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: true,
Aliases: []config.Alias{},
},
{
Name: "groupId",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "artifactId",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "globalSettingsFile",
ResourceRef: []config.ResourceReference{},
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{{Name: "maven/globalSettingsFile"}},
},
{
Name: "m2Path",
ResourceRef: []config.ResourceReference{},
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{{Name: "maven/m2Path"}},
},
{
Name: "additionalClassifiers",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "user",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "password",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
},
},
},
}
return theMetaData
}

View File

@ -0,0 +1,16 @@
package cmd
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNexusUploadCommand(t *testing.T) {
testCmd := NexusUploadCommand()
// only high level testing performed - details are tested in step generation procudure
assert.Equal(t, "nexusUpload", testCmd.Use, "command name incorrect")
}

732
cmd/nexusUpload_test.go Normal file
View File

@ -0,0 +1,732 @@
package cmd
import (
"fmt"
"github.com/SAP/jenkins-library/pkg/maven"
"github.com/SAP/jenkins-library/pkg/mock"
"github.com/SAP/jenkins-library/pkg/nexus"
"github.com/stretchr/testify/assert"
"os"
"testing"
)
type mockUtilsBundle struct {
mta bool
maven bool
files map[string][]byte
removedFiles map[string][]byte
properties map[string]map[string]string
cpe map[string]string
execRunner mock.ExecMockRunner
}
func newMockUtilsBundle(usesMta, usesMaven bool) mockUtilsBundle {
utils := mockUtilsBundle{mta: usesMta, maven: usesMaven}
utils.files = map[string][]byte{}
utils.removedFiles = map[string][]byte{}
utils.properties = map[string]map[string]string{}
utils.cpe = map[string]string{}
return utils
}
func (m *mockUtilsBundle) usesMta() bool {
return m.mta
}
func (m *mockUtilsBundle) usesMaven() bool {
return m.maven
}
func (m *mockUtilsBundle) fileExists(path string) (bool, error) {
content := m.files[path]
if content == nil {
return false, fmt.Errorf("'%s': %w", path, os.ErrNotExist)
}
return true, nil
}
func (m *mockUtilsBundle) fileRead(path string) ([]byte, error) {
content := m.files[path]
if content == nil {
return nil, fmt.Errorf("could not read '%s'", path)
}
return content, nil
}
func (m *mockUtilsBundle) fileWrite(path string, content []byte, _ os.FileMode) error {
m.files[path] = content
return nil
}
func (m *mockUtilsBundle) fileRemove(path string) {
contents := m.files[path]
m.files[path] = nil
if contents != nil {
m.removedFiles[path] = contents
}
}
func (m *mockUtilsBundle) getEnvParameter(path, name string) string {
path = path + "/" + name
return m.cpe[path]
}
func (m *mockUtilsBundle) getExecRunner() execRunner {
return &m.execRunner
}
func (m *mockUtilsBundle) setProperty(pomFile, expression, value string) {
pom := m.properties[pomFile]
if pom == nil {
pom = map[string]string{}
m.properties[pomFile] = pom
}
pom[expression] = value
}
func (m *mockUtilsBundle) evaluate(pomFile, expression string) (string, error) {
pom := m.properties[pomFile]
if pom == nil {
return "", fmt.Errorf("pom file '%s' not found", pomFile)
}
value := pom[expression]
if value == "<empty>" {
return "", nil
}
if value == "" {
return "", fmt.Errorf("property '%s' not found in '%s'", expression, pomFile)
}
return value, nil
}
type mockUploader struct {
nexus.Upload
uploadedArtifacts []nexus.ArtifactDescription
}
func (m *mockUploader) Clear() {
// Clear is called after a successful upload. Record the artifacts that are present before
// they are cleared. This way we can later peek into the set of all artifacts that were
// uploaded across multiple bundles.
m.uploadedArtifacts = append(m.uploadedArtifacts, m.GetArtifacts()...)
m.Upload.Clear()
}
func createOptions() nexusUploadOptions {
return nexusUploadOptions{
Repository: "maven-releases",
GroupID: "my.group.id",
ArtifactID: "artifact.id",
Version: "nexus3",
Url: "localhost:8081",
}
}
var testMtaYml = []byte(`
_schema-version: 2.1.0
ID: test
version: 0.3.0
modules:
- name: java
type: java
path: srv
`)
var testMtaYmlNoVersion = []byte(`
_schema-version: 2.1.0
ID: test
modules:
- name: java
type: java
`)
var testPomXml = []byte(`
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1.0</version>
</project>
`)
func TestUploadMTAProjects(t *testing.T) {
t.Run("Uploading MTA project without groupId parameter fails", func(t *testing.T) {
utils := newMockUtilsBundle(true, false)
utils.files["mta.yaml"] = testMtaYml
utils.cpe[".pipeline/commonPipelineEnvironment/mtarFilePath"] = "test.mtar"
uploader := mockUploader{}
options := createOptions()
options.GroupID = ""
err := runNexusUpload(&utils, &uploader, &options)
assert.EqualError(t, err, "the 'groupId' parameter needs to be provided for MTA projects")
assert.Equal(t, 0, len(uploader.GetArtifacts()))
assert.Equal(t, 0, len(uploader.uploadedArtifacts))
})
t.Run("Uploading MTA project without artifactId parameter fails", func(t *testing.T) {
utils := newMockUtilsBundle(true, false)
utils.files["mta.yaml"] = testMtaYml
utils.cpe[".pipeline/commonPipelineEnvironment/mtarFilePath"] = "test.mtar"
uploader := mockUploader{}
options := createOptions()
options.ArtifactID = ""
err := runNexusUpload(&utils, &uploader, &options)
assert.EqualError(t, err, "the 'artifactId' parameter was not provided and could not be retrieved from the Common Pipeline Environment")
assert.Equal(t, 0, len(uploader.GetArtifacts()))
assert.Equal(t, 0, len(uploader.uploadedArtifacts))
})
t.Run("Uploading MTA project fails due to missing yaml file", func(t *testing.T) {
utils := newMockUtilsBundle(true, false)
utils.cpe[".pipeline/commonPipelineEnvironment/mtarFilePath"] = "test.mtar"
uploader := mockUploader{}
options := createOptions()
err := runNexusUpload(&utils, &uploader, &options)
assert.EqualError(t, err, "could not read from required project descriptor file 'mta.yml'")
assert.Equal(t, 0, len(uploader.GetArtifacts()))
assert.Equal(t, 0, len(uploader.uploadedArtifacts))
})
t.Run("Uploading MTA project fails due to garbage YAML content", func(t *testing.T) {
utils := newMockUtilsBundle(true, false)
utils.files["mta.yaml"] = []byte("garbage")
utils.cpe[".pipeline/commonPipelineEnvironment/mtarFilePath"] = "test.mtar"
uploader := mockUploader{}
options := createOptions()
err := runNexusUpload(&utils, &uploader, &options)
assert.EqualError(t, err,
"failed to parse contents of the project descriptor file 'mta.yaml'")
assert.Equal(t, 0, len(uploader.GetArtifacts()))
assert.Equal(t, 0, len(uploader.uploadedArtifacts))
})
t.Run("Uploading MTA project fails due invalid version in YAML content", func(t *testing.T) {
utils := newMockUtilsBundle(true, false)
utils.files["mta.yaml"] = []byte(testMtaYmlNoVersion)
utils.cpe[".pipeline/commonPipelineEnvironment/mtarFilePath"] = "test.mtar"
uploader := mockUploader{}
options := createOptions()
err := runNexusUpload(&utils, &uploader, &options)
assert.EqualError(t, err,
"the project descriptor file 'mta.yaml' has an invalid version: version must not be empty")
assert.Equal(t, 0, len(uploader.GetArtifacts()))
assert.Equal(t, 0, len(uploader.uploadedArtifacts))
})
t.Run("Test uploading mta.yaml project fails due to missing mtar file", func(t *testing.T) {
utils := newMockUtilsBundle(true, false)
utils.files["mta.yaml"] = testMtaYml
utils.cpe[".pipeline/commonPipelineEnvironment/mtarFilePath"] = "test.mtar"
uploader := mockUploader{}
options := createOptions()
err := runNexusUpload(&utils, &uploader, &options)
assert.EqualError(t, err, "artifact file not found 'test.mtar'")
assert.Equal(t, "0.3.0", uploader.GetArtifactsVersion())
assert.Equal(t, "artifact.id", uploader.GetArtifactsID())
// Check the artifacts that /would/ have been uploaded
artifacts := uploader.GetArtifacts()
if assert.Equal(t, 1, len(artifacts)) {
assert.Equal(t, "mta.yaml", artifacts[0].File)
assert.Equal(t, "yaml", artifacts[0].Type)
}
assert.Equal(t, 0, len(uploader.uploadedArtifacts))
})
t.Run("Test uploading mta.yaml project works", func(t *testing.T) {
utils := newMockUtilsBundle(true, false)
utils.files["mta.yaml"] = testMtaYml
utils.files["test.mtar"] = []byte("contentsOfMtar")
utils.cpe[".pipeline/commonPipelineEnvironment/mtarFilePath"] = "test.mtar"
uploader := mockUploader{}
options := createOptions()
err := runNexusUpload(&utils, &uploader, &options)
assert.NoError(t, err, "expected mta.yaml project upload to work")
assert.Equal(t, "0.3.0", uploader.GetArtifactsVersion())
assert.Equal(t, "artifact.id", uploader.GetArtifactsID())
artifacts := uploader.uploadedArtifacts
if assert.Equal(t, 2, len(artifacts)) {
assert.Equal(t, "mta.yaml", artifacts[0].File)
assert.Equal(t, "yaml", artifacts[0].Type)
assert.Equal(t, "test.mtar", artifacts[1].File)
assert.Equal(t, "mtar", artifacts[1].Type)
}
})
t.Run("Test uploading mta.yml project works", func(t *testing.T) {
utils := newMockUtilsBundle(true, false)
utils.files["mta.yml"] = testMtaYml
utils.files["test.mtar"] = []byte("contentsOfMtar")
utils.cpe[".pipeline/commonPipelineEnvironment/mtarFilePath"] = "test.mtar"
uploader := mockUploader{}
options := createOptions()
err := runNexusUpload(&utils, &uploader, &options)
assert.NoError(t, err, "expected mta.yml project upload to work")
assert.Equal(t, "0.3.0", uploader.GetArtifactsVersion())
assert.Equal(t, "artifact.id", uploader.GetArtifactsID())
artifacts := uploader.uploadedArtifacts
if assert.Equal(t, 2, len(artifacts)) {
assert.Equal(t, "mta.yml", artifacts[0].File)
assert.Equal(t, "yaml", artifacts[0].Type)
assert.Equal(t, "test.mtar", artifacts[1].File)
assert.Equal(t, "mtar", artifacts[1].Type)
}
})
t.Run("Test uploading mta.yml project works with artifactID from CPE", func(t *testing.T) {
utils := newMockUtilsBundle(true, false)
utils.files["mta.yml"] = testMtaYml
utils.files["test.mtar"] = []byte("contentsOfMtar")
utils.cpe[".pipeline/commonPipelineEnvironment/mtarFilePath"] = "test.mtar"
utils.cpe[".pipeline/commonPipelineEnvironment/configuration/artifactId"] = "my-artifact-id"
uploader := mockUploader{}
options := createOptions()
// Clear artifact ID to trigger reading it from the CPE
options.ArtifactID = ""
err := runNexusUpload(&utils, &uploader, &options)
assert.NoError(t, err, "expected mta.yml project upload to work")
assert.Equal(t, "my-artifact-id", uploader.GetArtifactsID())
artifacts := uploader.uploadedArtifacts
if assert.Equal(t, 2, len(artifacts)) {
assert.Equal(t, "mta.yml", artifacts[0].File)
assert.Equal(t, "yaml", artifacts[0].Type)
assert.Equal(t, "test.mtar", artifacts[1].File)
assert.Equal(t, "mtar", artifacts[1].Type)
}
})
}
func TestUploadArtifacts(t *testing.T) {
t.Run("Uploading MTA project fails without info", func(t *testing.T) {
utils := newMockUtilsBundle(false, true)
uploader := mockUploader{}
options := createOptions()
err := uploadArtifacts(&utils, &uploader, &options, false)
assert.EqualError(t, err, "no group ID was provided, or could be established from project files")
})
t.Run("Uploading MTA project fails without any artifacts", func(t *testing.T) {
utils := newMockUtilsBundle(false, true)
uploader := mockUploader{}
options := createOptions()
_ = uploader.SetInfo(options.GroupID, "some.id", "3.0")
err := uploadArtifacts(&utils, &uploader, &options, false)
assert.EqualError(t, err, "no artifacts to upload")
})
t.Run("Uploading MTA project fails for unknown reasons", func(t *testing.T) {
utils := newMockUtilsBundle(false, true)
// Configure mocked execRunner to fail
utils.execRunner.ShouldFailOnCommand = map[string]error{}
utils.execRunner.ShouldFailOnCommand["mvn"] = fmt.Errorf("failed")
uploader := mockUploader{}
options := createOptions()
_ = uploader.SetInfo(options.GroupID, "some.id", "3.0")
_ = uploader.AddArtifact(nexus.ArtifactDescription{
File: "mta.yaml",
Type: "yaml",
})
_ = uploader.AddArtifact(nexus.ArtifactDescription{
File: "artifact.mtar",
Type: "yaml",
})
err := uploadArtifacts(&utils, &uploader, &options, false)
assert.EqualError(t, err, "uploading artifacts for ID 'some.id' failed: failed to run executable, command: '[mvn -Durl=http:// -DgroupId=my.group.id -Dversion=3.0 -DartifactId=some.id -Dfile=mta.yaml -Dpackaging=yaml -DgeneratePom=false -Dfiles=artifact.mtar -Dclassifiers= -Dtypes=yaml --batch-mode "+deployGoal+"]', error: failed")
})
t.Run("Uploading bundle generates correct maven parameters", func(t *testing.T) {
utils := newMockUtilsBundle(false, true)
uploader := mockUploader{}
options := createOptions()
_ = uploader.SetRepoURL("localhost:8081", "nexus3", "maven-releases")
_ = uploader.SetInfo(options.GroupID, "my.artifact", "4.0")
_ = uploader.AddArtifact(nexus.ArtifactDescription{
File: "mta.yaml",
Type: "yaml",
})
_ = uploader.AddArtifact(nexus.ArtifactDescription{
File: "pom.yml",
Type: "pom",
})
err := uploadArtifacts(&utils, &uploader, &options, false)
assert.NoError(t, err, "expected upload as two bundles to work")
assert.Equal(t, 1, len(utils.execRunner.Calls))
expectedParameters1 := []string{
"-Durl=http://localhost:8081/repository/maven-releases/",
"-DgroupId=my.group.id",
"-Dversion=4.0",
"-DartifactId=my.artifact",
"-Dfile=mta.yaml",
"-Dpackaging=yaml",
"-DgeneratePom=false",
"-Dfiles=pom.yml",
"-Dclassifiers=",
"-Dtypes=pom",
"--batch-mode",
deployGoal}
assert.Equal(t, len(expectedParameters1), len(utils.execRunner.Calls[0].Params))
assert.Equal(t, mock.ExecCall{Exec: "mvn", Params: expectedParameters1}, utils.execRunner.Calls[0])
})
}
func TestUploadMavenProjects(t *testing.T) {
t.Run("Uploading Maven project fails due to missing pom.xml", func(t *testing.T) {
utils := newMockUtilsBundle(false, true)
uploader := mockUploader{}
options := createOptions()
err := runNexusUpload(&utils, &uploader, &options)
assert.EqualError(t, err, "pom.xml not found")
assert.Equal(t, 0, len(uploader.uploadedArtifacts))
})
t.Run("Test uploading Maven project with POM packaging works", func(t *testing.T) {
utils := newMockUtilsBundle(false, true)
utils.setProperty("pom.xml", "project.version", "1.0")
utils.setProperty("pom.xml", "project.groupId", "com.mycompany.app")
utils.setProperty("pom.xml", "project.artifactId", "my-app")
utils.setProperty("pom.xml", "project.packaging", "pom")
utils.setProperty("pom.xml", "project.build.finalName", "my-app-1.0")
utils.files["pom.xml"] = testPomXml
uploader := mockUploader{}
options := createOptions()
err := runNexusUpload(&utils, &uploader, &options)
assert.NoError(t, err, "expected Maven upload to work")
assert.Equal(t, "1.0", uploader.GetArtifactsVersion())
assert.Equal(t, "my-app", uploader.GetArtifactsID())
artifacts := uploader.uploadedArtifacts
if assert.Equal(t, 1, len(artifacts)) {
assert.Equal(t, "pom.xml", artifacts[0].File)
assert.Equal(t, "pom", artifacts[0].Type)
}
})
t.Run("Test uploading Maven project with JAR packaging works", func(t *testing.T) {
utils := newMockUtilsBundle(false, true)
utils.setProperty("pom.xml", "project.version", "1.0")
utils.setProperty("pom.xml", "project.groupId", "com.mycompany.app")
utils.setProperty("pom.xml", "project.artifactId", "my-app")
utils.setProperty("pom.xml", "project.packaging", "jar")
utils.setProperty("pom.xml", "project.build.finalName", "my-app-1.0")
utils.files["pom.xml"] = testPomXml
utils.files["target/my-app-1.0.jar"] = []byte("contentsOfJar")
uploader := mockUploader{}
options := createOptions()
err := runNexusUpload(&utils, &uploader, &options)
assert.NoError(t, err, "expected Maven upload to work")
assert.Equal(t, "1.0", uploader.GetArtifactsVersion())
assert.Equal(t, "my-app", uploader.GetArtifactsID())
artifacts := uploader.uploadedArtifacts
if assert.Equal(t, 2, len(artifacts)) {
assert.Equal(t, "pom.xml", artifacts[0].File)
assert.Equal(t, "pom", artifacts[0].Type)
assert.Equal(t, "target/my-app-1.0.jar", artifacts[1].File)
assert.Equal(t, "jar", artifacts[1].Type)
}
})
t.Run("Test uploading Maven project with fall-back to JAR packaging works", func(t *testing.T) {
utils := newMockUtilsBundle(false, true)
utils.setProperty("pom.xml", "project.version", "1.0")
utils.setProperty("pom.xml", "project.groupId", "com.mycompany.app")
utils.setProperty("pom.xml", "project.artifactId", "my-app")
utils.setProperty("pom.xml", "project.packaging", "<empty>")
utils.setProperty("pom.xml", "project.build.finalName", "my-app-1.0")
utils.files["pom.xml"] = testPomXml
utils.files["target/my-app-1.0.jar"] = []byte("contentsOfJar")
uploader := mockUploader{}
options := createOptions()
err := runNexusUpload(&utils, &uploader, &options)
assert.NoError(t, err, "expected Maven upload to work")
assert.Equal(t, "1.0", uploader.GetArtifactsVersion())
assert.Equal(t, "my-app", uploader.GetArtifactsID())
artifacts := uploader.uploadedArtifacts
if assert.Equal(t, 2, len(artifacts)) {
assert.Equal(t, "pom.xml", artifacts[0].File)
assert.Equal(t, "pom", artifacts[0].Type)
assert.Equal(t, "target/my-app-1.0.jar", artifacts[1].File)
assert.Equal(t, "jar", artifacts[1].Type)
}
})
t.Run("Test uploading Maven project with fall-back to group id from parameters works", func(t *testing.T) {
utils := newMockUtilsBundle(false, true)
utils.setProperty("pom.xml", "project.version", "1.0")
utils.setProperty("pom.xml", "project.artifactId", "my-app")
utils.setProperty("pom.xml", "project.packaging", "pom")
utils.setProperty("pom.xml", "project.build.finalName", "my-app-1.0")
utils.files["pom.xml"] = testPomXml
uploader := mockUploader{}
options := createOptions()
options.GroupID = "awesome.group"
err := runNexusUpload(&utils, &uploader, &options)
assert.NoError(t, err, "expected Maven upload to work")
assert.Equal(t, "localhost:8081/repository/maven-releases/",
uploader.GetRepoURL())
assert.Equal(t, "1.0", uploader.GetArtifactsVersion())
assert.Equal(t, "my-app", uploader.GetArtifactsID())
artifacts := uploader.uploadedArtifacts
if assert.Equal(t, 1, len(artifacts)) {
assert.Equal(t, "pom.xml", artifacts[0].File)
assert.Equal(t, "pom", artifacts[0].Type)
}
})
t.Run("Test uploading Maven project with application module and finalName works", func(t *testing.T) {
utils := newMockUtilsBundle(false, true)
utils.setProperty("pom.xml", "project.version", "1.0")
utils.setProperty("pom.xml", "project.groupId", "com.mycompany.app")
utils.setProperty("pom.xml", "project.artifactId", "my-app")
utils.setProperty("pom.xml", "project.packaging", "pom")
utils.setProperty("pom.xml", "project.build.finalName", "my-app-1.0")
utils.setProperty("application/pom.xml", "project.version", "1.0")
utils.setProperty("application/pom.xml", "project.groupId", "com.mycompany.app")
utils.setProperty("application/pom.xml", "project.artifactId", "my-app-app")
utils.setProperty("application/pom.xml", "project.packaging", "jar")
utils.setProperty("application/pom.xml", "project.build.finalName", "final-artifact")
utils.files["pom.xml"] = testPomXml
utils.files["application/pom.xml"] = testPomXml
utils.files["application/target/final-artifact.jar"] = []byte("contentsOfJar")
utils.files["application/target/final-artifact-classes.jar"] = []byte("contentsOfClassesJar")
uploader := mockUploader{}
options := createOptions()
options.AdditionalClassifiers = `
[
{
"classifier" : "classes",
"type" : "jar"
}
]
`
err := runNexusUpload(&utils, &uploader, &options)
assert.NoError(t, err, "expected upload of maven project with application module to succeed")
assert.Equal(t, "1.0", uploader.GetArtifactsVersion())
assert.Equal(t, "my-app-app", uploader.GetArtifactsID())
artifacts := uploader.uploadedArtifacts
if assert.Equal(t, 4, len(artifacts)) {
assert.Equal(t, "pom.xml", artifacts[0].File)
assert.Equal(t, "pom", artifacts[0].Type)
assert.Equal(t, "application/pom.xml", artifacts[1].File)
assert.Equal(t, "pom", artifacts[1].Type)
assert.Equal(t, "application/target/final-artifact.jar", artifacts[2].File)
assert.Equal(t, "jar", artifacts[2].Type)
assert.Equal(t, "application/target/final-artifact-classes.jar", artifacts[3].File)
assert.Equal(t, "jar", artifacts[3].Type)
}
if assert.Equal(t, 2, len(utils.execRunner.Calls)) {
expectedParameters1 := []string{
"-Durl=http://localhost:8081/repository/maven-releases/",
"-DgroupId=com.mycompany.app",
"-Dversion=1.0",
"-DartifactId=my-app",
"-Dfile=pom.xml",
"-Dpackaging=pom",
"--batch-mode",
deployGoal}
assert.Equal(t, len(expectedParameters1), len(utils.execRunner.Calls[0].Params))
assert.Equal(t, mock.ExecCall{Exec: "mvn", Params: expectedParameters1}, utils.execRunner.Calls[0])
expectedParameters2 := []string{
"-Durl=http://localhost:8081/repository/maven-releases/",
"-DgroupId=com.mycompany.app",
"-Dversion=1.0",
"-DartifactId=my-app-app",
"-Dfile=application/pom.xml",
"-Dpackaging=pom",
"-Dfiles=application/target/final-artifact.jar,application/target/final-artifact-classes.jar",
"-Dclassifiers=,classes",
"-Dtypes=jar,jar",
"--batch-mode",
deployGoal}
assert.Equal(t, len(expectedParameters2), len(utils.execRunner.Calls[1].Params))
assert.Equal(t, mock.ExecCall{Exec: "mvn", Params: expectedParameters2}, utils.execRunner.Calls[1])
}
})
t.Run("Test uploading Maven project fails without packaging", func(t *testing.T) {
utils := newMockUtilsBundle(false, true)
utils.setProperty("pom.xml", "project.version", "1.0")
utils.setProperty("pom.xml", "project.groupId", "com.mycompany.app")
utils.setProperty("pom.xml", "project.artifactId", "my-app")
utils.files["pom.xml"] = testPomXml
utils.files["target/my-app-1.0.jar"] = []byte("contentsOfJar")
uploader := mockUploader{}
options := createOptions()
err := runNexusUpload(&utils, &uploader, &options)
assert.EqualError(t, err, "property 'project.packaging' not found in 'pom.xml'")
artifacts := uploader.GetArtifacts()
if assert.Equal(t, 1, len(artifacts)) {
assert.Equal(t, "pom.xml", artifacts[0].File)
assert.Equal(t, "pom", artifacts[0].Type)
}
assert.Equal(t, 0, len(uploader.uploadedArtifacts))
})
t.Run("Write credentials settings", func(t *testing.T) {
utils := newMockUtilsBundle(false, true)
utils.setProperty("pom.xml", "project.version", "1.0")
utils.setProperty("pom.xml", "project.groupId", "com.mycompany.app")
utils.setProperty("pom.xml", "project.artifactId", "my-app")
utils.setProperty("pom.xml", "project.packaging", "pom")
utils.setProperty("pom.xml", "project.build.finalName", "my-app-1.0")
utils.files["pom.xml"] = testPomXml
uploader := mockUploader{}
options := createOptions()
options.User = "admin"
options.Password = "admin123"
err := runNexusUpload(&utils, &uploader, &options)
assert.NoError(t, err, "expected Maven upload to work")
assert.Equal(t, 1, len(utils.execRunner.Calls))
expectedParameters1 := []string{
"--settings",
settingsPath,
"-Durl=http://localhost:8081/repository/maven-releases/",
"-DgroupId=com.mycompany.app",
"-Dversion=1.0",
"-DartifactId=my-app",
"-DrepositoryId=" + settingsServerID,
"-Dfile=pom.xml",
"-Dpackaging=pom",
"--batch-mode",
deployGoal}
assert.Equal(t, len(expectedParameters1), len(utils.execRunner.Calls[0].Params))
assert.Equal(t, mock.ExecCall{Exec: "mvn", Params: expectedParameters1}, utils.execRunner.Calls[0])
expectedEnv := []string{"NEXUS_username=admin", "NEXUS_password=admin123"}
assert.Equal(t, 2, len(utils.execRunner.Env))
assert.Equal(t, expectedEnv, utils.execRunner.Env)
assert.Nil(t, utils.files[settingsPath])
assert.NotNil(t, utils.removedFiles[settingsPath])
})
}
func TestUploadUnknownProjectFails(t *testing.T) {
utils := newMockUtilsBundle(false, false)
uploader := mockUploader{}
options := createOptions()
err := runNexusUpload(&utils, &uploader, &options)
assert.EqualError(t, err, "unsupported project structure")
}
func TestAdditionalClassifierEmpty(t *testing.T) {
t.Run("Empty additional classifiers", func(t *testing.T) {
utils := newMockUtilsBundle(false, false)
client, err := testAdditionalClassifierArtifacts(&utils, "")
assert.NoError(t, err, "expected empty additional classifiers to succeed")
assert.Equal(t, 0, len(client.GetArtifacts()))
})
t.Run("Additional classifiers is invalid JSON", func(t *testing.T) {
utils := newMockUtilsBundle(false, false)
client, err := testAdditionalClassifierArtifacts(&utils, "some random string")
assert.Error(t, err, "expected invalid additional classifiers to fail")
assert.Equal(t, 0, len(client.GetArtifacts()))
})
t.Run("Classifiers valid but wrong JSON", func(t *testing.T) {
json := `
[
{
"classifier" : "source",
"type" : "jar"
},
{}
]
`
utils := newMockUtilsBundle(false, false)
utils.files["some folder/artifact-id-source.jar"] = []byte("contentsOfJar")
client, err := testAdditionalClassifierArtifacts(&utils, json)
assert.Error(t, err, "expected invalid additional classifiers to fail")
assert.Equal(t, 1, len(client.GetArtifacts()))
})
t.Run("Classifiers valid but does not exist", func(t *testing.T) {
json := `
[
{
"classifier" : "source",
"type" : "jar"
}
]
`
utils := newMockUtilsBundle(false, false)
client, err := testAdditionalClassifierArtifacts(&utils, json)
assert.EqualError(t, err, "artifact file not found 'some folder/artifact-id-source.jar'")
assert.Equal(t, 0, len(client.GetArtifacts()))
})
t.Run("Additional classifiers is valid JSON", func(t *testing.T) {
json := `
[
{
"classifier" : "source",
"type" : "jar"
},
{
"classifier" : "classes",
"type" : "jar"
}
]
`
utils := newMockUtilsBundle(false, false)
utils.files["some folder/artifact-id-source.jar"] = []byte("contentsOfJar")
utils.files["some folder/artifact-id-classes.jar"] = []byte("contentsOfJar")
client, err := testAdditionalClassifierArtifacts(&utils, json)
assert.NoError(t, err, "expected valid additional classifiers to succeed")
assert.Equal(t, 2, len(client.GetArtifacts()))
})
}
func testAdditionalClassifierArtifacts(utils nexusUploadUtils, additionalClassifiers string) (*nexus.Upload, error) {
client := nexus.Upload{}
_ = client.SetInfo("group.id", "artifact-id", "1.0")
return &client, addMavenTargetSubArtifacts(utils, &client, additionalClassifiers,
"some folder", "artifact-id")
}
func TestSetupNexusCredentialsSettingsFile(t *testing.T) {
utils := newMockUtilsBundle(false, true)
options := nexusUploadOptions{User: "admin", Password: "admin123"}
mavenOptions := maven.ExecuteOptions{}
settingsPath, err := setupNexusCredentialsSettingsFile(&utils, &options, &mavenOptions)
assert.NoError(t, err, "expected setting up credentials settings.xml to work")
assert.Equal(t, 0, len(utils.execRunner.Calls))
expectedEnv := []string{"NEXUS_username=admin", "NEXUS_password=admin123"}
assert.Equal(t, 2, len(utils.execRunner.Env))
assert.Equal(t, expectedEnv, utils.execRunner.Env)
assert.True(t, settingsPath != "")
assert.NotNil(t, utils.files[settingsPath])
}

View File

@ -61,6 +61,7 @@ func Execute() {
rootCmd.AddCommand(MavenExecuteCommand())
rootCmd.AddCommand(MavenBuildCommand())
rootCmd.AddCommand(MavenExecuteStaticCodeChecksCommand())
rootCmd.AddCommand(NexusUploadCommand())
addRootFlags(rootCmd)
if err := rootCmd.Execute(); err != nil {

View File

@ -0,0 +1,81 @@
# Artifact Deployment to Nexus
## Status
Accepted
## Context
The nexusUpload step shall upload (deploy) build artifacts to a Nexus Repository Manager. Nexus version 2 and 3 need to be supported.
Per module, there can be an artifact and multiple sub-artifacts, which need to be deployed as a unit, optionally together with the project descriptor file (i.e. pom.xml or mta.yaml).
A Nexus contains repositories of different type. For example, a "release" repository does not allow updating existing artifacts, while a "snapshot" repository allows for multiple builds of the same snapshot version, with the notion of a "latest" build.
Depending on the type of repository, a certain directory layout has to be obeyed, and `maven-metadata.xml` files have to be maintained in order to be compatible with tools consuming the artifacts. The Nexus itself may also have mechanisms in place, for example to automatically purge old builds in snapshot releases.
All this makes it important to make compatible deployments.
### Alternatives
* [Apache Maven Deploy Plugin](http://maven.apache.org/plugins/maven-deploy-plugin/)
* Maven lifecycle phase : deploy
* Uploading artifacts manually
### Pros and Cons
#### Apache Maven Deploy Plugin (deploy:deploy-file)
For this option, we only consider the goal `deploy:deploy-file`.
##### :+1:
- Official maven plugin for deployment, which is perfect if you only care whether the artifacts are deployed correctly.
##### :-1:
- Knowledge about which artifacts to deploy has to be obtained manually.
- A list of parameters has to be generated before using the plugin, including `artifactId` and `version`, which is the same case as the `Uploading artifacts manually`. For maven projects, the parameters can be obtained using the `evaluate` goal of the `maven-help-plugin`. There is however a performance impact, since a maven command line has to be executed for each parameter, multiplied by the number of modules. This is not a problem for `Maven lifecycle phase : deploy`.
- Credential info has to be stored in a `settings.xml`, which introduces additional implementation. Credentials can be passed via environment variables.
#### Maven lifecycle phase: deploy
By default, the maven lifecycle phase `deploy` binds to the goal `deploy:deploy` of the `Apache Maven Deploy Plugin`.
##### :+1:
- Same as the `Apache Maven Deploy Plugin`
- You don't have to obtain and pass the parameters as for `Apache Maven Deploy Plugin`, because `package` phase is executed implicitly and makes the parameters ready before `deploy` phase.
- Supports multi-module Maven projects and any project structure.
##### :-1:
- Same case as the `Apache Maven Deploy Plugin` for handling credentials.
- Cannot be used for non-Maven projects (i.e. MTA)
- As a maven phase, a list of phases is triggered implicitly before this phase, including `compile`, `test` and `package`.
To follow the build-once principle, all these phases have to be skipped.
However, it's not possible to skip some of the maven goals binding to certain phases.
For example, if the `<packaging>` tag of the `pom.xml` is set to `jar`, then the `jar:jar` goal of the [`Apache Maven JAR Plugin`](https://maven.apache.org/plugins/maven-jar-plugin/) is bound to the `package` phase.
Unfortunately, however, `Apache Maven JAR Plugin` does not provide an option to skip the the `jar:jar` goal. There may be a [solution](https://stackoverflow.com/questions/47673545/how-to-skip-jar-deploy-in-maven-and-deploy-the-assembly-only), but it seems to require modifying the pom and could also be different depending on the used packaging.
**This is the main reason why we cannot use this option.**
#### Uploading artifacts manually
Files can be uploaded to the Nexus by simple HTTP PUT requests, using basic authentication if necessary. Meta-data files have to be downloaded, updated and re-uploaded after successful upload of the artifacts.
##### :+1:
- Without the pain of handling the credentials, which was mentioned above in `Apache Maven Deploy Plugin` section.
- Gives full control over the implementation.
##### :-1:
- Same as the `Apache Maven Deploy Plugin`. Knowledge about which artifacts to deploy has to be obtained manually.
- Same as the `Apache Maven Deploy Plugin`. A list of parameters has to be prepared.
- Introduces complexity for maintaining maven-metadata.xml. For example there is a great difference between "release" and "snapshot" deployments. The later have a build number and another directory structure on the nexus (arbitrary number of builds per version, with metadata for each build and for the version).
- Has the greatest maintenance-overhead.
### Decision
`Apache Maven Deploy Plugin` is chosen, because:
- `Maven lifecycle phase: deploy` conflicts with the build-once principle.
- Credentials handling is not very complex to implement.
- Has the fine-grained control needed over which artifacts are deployed.
- Maintains maven-metadata.xml correctly for various types of deployments.

2
go.mod
View File

@ -17,7 +17,7 @@ require (
github.com/spf13/cobra v0.0.5
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.4.0
github.com/testcontainers/testcontainers-go v0.1.0
github.com/testcontainers/testcontainers-go v0.2.0
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v2 v2.2.4

4
go.sum
View File

@ -310,8 +310,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/testcontainers/testcontainers-go v0.1.0 h1:R7CZ/dgrendXg5T+gPffAZ2sRQq38kqdPelICJshXLo=
github.com/testcontainers/testcontainers-go v0.1.0/go.mod h1:5aBi+1PJmFixVc3b349A7NrhyTRYkMDpZEtT5MFxCs8=
github.com/testcontainers/testcontainers-go v0.2.0 h1:7PED4vniZMXNkPln4zg9U9ZDp4MmD/LuX0LGYTnByE0=
github.com/testcontainers/testcontainers-go v0.2.0/go.mod h1:5aBi+1PJmFixVc3b349A7NrhyTRYkMDpZEtT5MFxCs8=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=

View File

@ -0,0 +1,156 @@
// +build integration
// can be execute with go test -tags=integration ./integration/...
package main
import (
"context"
"fmt"
"net/http"
"testing"
"time"
"github.com/SAP/jenkins-library/pkg/command"
"github.com/stretchr/testify/assert"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
func TestNexusUpload(t *testing.T) {
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "sonatype/nexus3:3.21.1",
ExposedPorts: []string{"8081/tcp"},
Env: map[string]string{"NEXUS_SECURITY_RANDOMPASSWORD": "false"},
WaitingFor: wait.ForLog("Started Sonatype Nexus").WithStartupTimeout(5 * time.Minute), // Nexus takes more than one minute to boot
}
nexusContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
assert.NoError(t, err)
defer nexusContainer.Terminate(ctx)
ip, err := nexusContainer.Host(ctx)
assert.NoError(t, err)
port, err := nexusContainer.MappedPort(ctx, "8081")
assert.NoError(t, err, "Could not map port for nexus container")
nexusIpAndPort := fmt.Sprintf("%s:%s", ip, port.Port())
url := "http://" + nexusIpAndPort
resp, err := http.Get(url)
assert.Equal(t, resp.StatusCode, http.StatusOK)
cmd := command.Command{}
cmd.SetDir("testdata/TestNexusIntegration/mta")
piperOptions := []string{
"nexusUpload",
"--groupId=mygroup",
"--artifactId=mymta",
"--user=admin",
"--password=admin123",
"--repository=maven-releases",
"--url=" + nexusIpAndPort,
}
err = cmd.RunExecutable(getPiperExecutable(), piperOptions...)
assert.NoError(t, err, "Calling piper with arguments %v failed.", piperOptions)
cmd = command.Command{}
cmd.SetDir("testdata/TestNexusIntegration/maven")
piperOptions = []string{
"nexusUpload",
"--user=admin",
"--password=admin123",
"--repository=maven-releases",
"--url=" + nexusIpAndPort,
}
err = cmd.RunExecutable(getPiperExecutable(), piperOptions...)
assert.NoError(t, err, "Calling piper with arguments %v failed.", piperOptions)
resp, err = http.Get(url + "/repository/maven-releases/com/mycompany/app/my-app/1.0/my-app-1.0.pom")
assert.NoError(t, err, "Downloading artifact failed")
assert.Equal(t, http.StatusOK, resp.StatusCode, "Get my-app-1.0.pom: %s", resp.Status)
resp, err = http.Get(url + "/repository/maven-releases/com/mycompany/app/my-app/1.0/my-app-1.0.jar")
assert.NoError(t, err, "Downloading artifact failed")
assert.Equal(t, http.StatusOK, resp.StatusCode, "Get my-app-1.0.jar: %s", resp.Status)
resp, err = http.Get(url + "/repository/maven-releases/mygroup/mymta/0.3.0/mymta-0.3.0.yaml")
assert.NoError(t, err, "Downloading artifact failed")
assert.Equal(t, http.StatusOK, resp.StatusCode, "Get mymta-0.3.0.yaml: %s", resp.Status)
resp, err = http.Get(url + "/repository/maven-releases/mygroup/mymta/0.3.0/mymta-0.3.0.mtar")
assert.NoError(t, err, "Downloading artifact failed")
assert.Equal(t, http.StatusOK, resp.StatusCode, "Get mymta-0.3.0.mtar: %s", resp.Status)
}
func TestNexus2Upload(t *testing.T) {
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "sonatype/nexus:2.14.16-01",
ExposedPorts: []string{"8081/tcp"},
WaitingFor: wait.ForLog("org.sonatype.nexus.bootstrap.jetty.JettyServer - Running").WithStartupTimeout(5 * time.Minute), // Nexus takes more than one minute to boot
}
nexusContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
assert.NoError(t, err)
defer nexusContainer.Terminate(ctx)
ip, err := nexusContainer.Host(ctx)
assert.NoError(t, err)
port, err := nexusContainer.MappedPort(ctx, "8081")
assert.NoError(t, err, "Could not map port for nexus container")
nexusIpAndPort := fmt.Sprintf("%s:%s", ip, port.Port())
url := "http://" + nexusIpAndPort + "/nexus/"
cmd := command.Command{}
cmd.SetDir("testdata/TestNexusIntegration/mta")
piperOptions := []string{
"nexusUpload",
"--groupId=mygroup",
"--artifactId=mymta",
"--user=admin",
"--password=admin123",
"--repository=releases",
"--version=nexus2",
"--url=" + nexusIpAndPort + "/nexus/",
}
err = cmd.RunExecutable(getPiperExecutable(), piperOptions...)
assert.NoError(t, err, "Calling piper with arguments %v failed.", piperOptions)
cmd = command.Command{}
cmd.SetDir("testdata/TestNexusIntegration/maven")
piperOptions = []string{
"nexusUpload",
"--user=admin",
"--password=admin123",
"--repository=releases",
"--version=nexus2",
"--url=" + nexusIpAndPort + "/nexus/",
}
err = cmd.RunExecutable(getPiperExecutable(), piperOptions...)
assert.NoError(t, err, "Calling piper with arguments %v failed.", piperOptions)
resp, err := http.Get(url + "content/repositories/releases/com/mycompany/app/my-app/1.0/my-app-1.0.pom")
assert.NoError(t, err, "Downloading artifact failed")
assert.Equal(t, http.StatusOK, resp.StatusCode, "Get my-app-1.0.pom: %s", resp.Status)
resp, err = http.Get(url + "content/repositories/releases/com/mycompany/app/my-app/1.0/my-app-1.0.jar")
assert.NoError(t, err, "Downloading artifact failed")
assert.Equal(t, http.StatusOK, resp.StatusCode, "Get my-app-1.0.jar: %s", resp.Status)
resp, err = http.Get(url + "content/repositories/releases/mygroup/mymta/0.3.0/mymta-0.3.0.yaml")
assert.NoError(t, err, "Downloading artifact failed")
assert.Equal(t, http.StatusOK, resp.StatusCode, "Get mymta-0.3.0.yaml: %s", resp.Status)
resp, err = http.Get(url + "content/repositories/releases/mygroup/mymta/0.3.0/mymta-0.3.0.mtar")
assert.NoError(t, err, "Downloading artifact failed")
assert.Equal(t, http.StatusOK, resp.StatusCode, "Get mymta-0.3.0.mtar: %s", resp.Status)
}

View File

@ -0,0 +1,7 @@
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
</project>

View File

@ -0,0 +1 @@
test.mtar

View File

@ -0,0 +1,10 @@
_schema-version: 2.1.0
ID: test
version: 0.3.0
modules:
- name: java
type: java
path: srv

Binary file not shown.

View File

@ -1,78 +1,106 @@
package nexus
import (
"crypto/md5"
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"hash"
"io"
"net/http"
"os"
"strings"
piperHttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/sirupsen/logrus"
"strings"
)
// ArtifactDescription describes a single artifact that can be uploaded to a Nexus repository manager.
// The File string must point to an existing file. The Classifier can be empty.
type ArtifactDescription struct {
ID string `json:"artifactId"`
Classifier string `json:"classifier"`
Type string `json:"type"`
File string `json:"file"`
}
// Upload holds state for an upload session. Call SetBaseURL(), SetArtifactsVersion() and add at least
// one artifact via AddArtifact(). Then call UploadArtifacts().
// Upload combines information about an artifact and its sub-artifacts which are supposed to be uploaded together.
// Call SetRepoURL(), SetArtifactsVersion(), SetArtifactID(), and add at least one artifact via AddArtifact().
type Upload struct {
baseURL string
version string
Username string
Password string
artifacts []ArtifactDescription
Logger *logrus.Entry
repoURL string
groupID string
version string
artifactID string
artifacts []ArtifactDescription
}
// Uploader provides an interface to the nexus upload for configuring the target Nexus Repository and
// adding artifacts.
// Uploader provides an interface for configuring the target Nexus Repository and adding artifacts.
type Uploader interface {
SetBaseURL(nexusURL, nexusVersion, repository, groupID string) error
SetArtifactsVersion(version string) error
SetRepoURL(nexusURL, nexusVersion, repository string) error
GetRepoURL() string
SetInfo(groupID, artifactsID, version string) error
GetGroupID() string
GetArtifactsID() string
GetArtifactsVersion() string
AddArtifact(artifact ArtifactDescription) error
GetArtifacts() []ArtifactDescription
UploadArtifacts() error
Clear()
}
func (nexusUpload *Upload) initLogger() {
if nexusUpload.Logger == nil {
nexusUpload.Logger = log.Entry().WithField("package", "SAP/jenkins-library/pkg/nexus")
}
}
// SetBaseURL constructs the base URL for all uploaded artifacts. No parameter can be empty.
func (nexusUpload *Upload) SetBaseURL(nexusURL, nexusVersion, repository, groupID string) error {
baseURL, err := getBaseURL(nexusURL, nexusVersion, repository, groupID)
// SetRepoURL constructs the base URL to the Nexus repository. No parameter can be empty.
func (nexusUpload *Upload) SetRepoURL(nexusURL, nexusVersion, repository string) error {
repoURL, err := getBaseURL(nexusURL, nexusVersion, repository)
if err != nil {
return err
}
nexusUpload.baseURL = baseURL
nexusUpload.repoURL = repoURL
return nil
}
// SetArtifactsVersion sets the common version for all uploaded artifacts. The version is external to
// GetRepoURL returns the base URL for the nexus repository.
func (nexusUpload *Upload) GetRepoURL() string {
return nexusUpload.repoURL
}
// ErrEmptyGroupID is returned from SetInfo, if groupID is empty.
var ErrEmptyGroupID = errors.New("groupID must not be empty")
// ErrEmptyArtifactID is returned from SetInfo, if artifactID is empty.
var ErrEmptyArtifactID = errors.New("artifactID must not be empty")
// ErrInvalidArtifactID is returned from SetInfo, if artifactID contains slashes.
var ErrInvalidArtifactID = errors.New("artifactID may not include slashes")
// ErrEmptyVersion is returned from SetInfo, if version is empty.
var ErrEmptyVersion = errors.New("version must not be empty")
// SetInfo sets the common info for all uploaded artifacts. This info is external to
// the artifact descriptions so that it is consistent for all of them.
func (nexusUpload *Upload) SetArtifactsVersion(version string) error {
if version == "" {
return errors.New("version must not be empty")
func (nexusUpload *Upload) SetInfo(groupID, artifactID, version string) error {
if groupID == "" {
return ErrEmptyGroupID
}
if artifactID == "" {
return ErrEmptyArtifactID
}
if strings.Contains(artifactID, "/") {
return ErrInvalidArtifactID
}
if version == "" {
return ErrEmptyVersion
}
nexusUpload.groupID = groupID
nexusUpload.artifactID = artifactID
nexusUpload.version = version
return nil
}
// GetArtifactsVersion returns the common version for all artifacts.
func (nexusUpload *Upload) GetArtifactsVersion() string {
return nexusUpload.version
}
// GetGroupID returns the common groupId for all artifacts.
func (nexusUpload *Upload) GetGroupID() string {
return nexusUpload.groupID
}
// GetArtifactsID returns the common artifactId for all artifacts.
func (nexusUpload *Upload) GetArtifactsID() string {
return nexusUpload.artifactID
}
// AddArtifact adds a single artifact to be uploaded later via UploadArtifacts(). If an identical artifact
// description is already contained in the Upload, the function does nothing and returns no error.
func (nexusUpload *Upload) AddArtifact(artifact ArtifactDescription) error {
@ -89,12 +117,9 @@ func (nexusUpload *Upload) AddArtifact(artifact ArtifactDescription) error {
}
func validateArtifact(artifact ArtifactDescription) error {
if artifact.File == "" || artifact.ID == "" || artifact.Type == "" {
return fmt.Errorf("Artifact.File (%v), ID (%v) or Type (%v) is empty",
artifact.File, artifact.ID, artifact.Type)
}
if strings.Contains(artifact.ID, "/") {
return fmt.Errorf("Artifact.ID may not include slashes")
if artifact.File == "" || artifact.Type == "" {
return fmt.Errorf("Artifact.File (%v) or Type (%v) is empty",
artifact.File, artifact.Type)
}
return nil
}
@ -115,60 +140,12 @@ func (nexusUpload *Upload) GetArtifacts() []ArtifactDescription {
return artifacts
}
// UploadArtifacts performs the actual upload of all added artifacts to the Nexus server.
func (nexusUpload *Upload) UploadArtifacts() error {
client := nexusUpload.createHTTPClient()
return nexusUpload.uploadArtifacts(client)
// Clear removes any contained artifact descriptions.
func (nexusUpload *Upload) Clear() {
nexusUpload.artifacts = []ArtifactDescription{}
}
func (nexusUpload *Upload) uploadArtifacts(client piperHttp.Sender) error {
if nexusUpload.baseURL == "" {
return fmt.Errorf("the nexus.Upload needs to be configured by calling SetBaseURL() first")
}
if nexusUpload.version == "" {
return fmt.Errorf("the nexus.Upload needs to be configured by calling SetArtifactsVersion() first")
}
if len(nexusUpload.artifacts) == 0 {
return fmt.Errorf("no artifacts to upload, call AddArtifact() first")
}
nexusUpload.initLogger()
for _, artifact := range nexusUpload.artifacts {
url := getArtifactURL(nexusUpload.baseURL, nexusUpload.version, artifact)
var err error
err = uploadHash(client, artifact.File, url+".md5", md5.New(), 16)
if err != nil {
return err
}
err = uploadHash(client, artifact.File, url+".sha1", sha1.New(), 20)
if err != nil {
return err
}
err = uploadFile(client, artifact.File, url)
if err != nil {
return err
}
}
// Reset all artifacts already uploaded, so the object could be re-used
nexusUpload.artifacts = nil
return nil
}
func (nexusUpload *Upload) createHTTPClient() *piperHttp.Client {
client := piperHttp.Client{}
clientOptions := piperHttp.ClientOptions{
Username: nexusUpload.Username,
Password: nexusUpload.Password,
Logger: nexusUpload.Logger,
}
client.SetOptions(clientOptions)
return &client
}
func getBaseURL(nexusURL, nexusVersion, repository, groupID string) (string, error) {
func getBaseURL(nexusURL, nexusVersion, repository string) (string, error) {
if nexusURL == "" {
return "", errors.New("nexusURL must not be empty")
}
@ -179,9 +156,6 @@ func getBaseURL(nexusURL, nexusVersion, repository, groupID string) (string, err
if repository == "" {
return "", errors.New("repository must not be empty")
}
if groupID == "" {
return "", errors.New("groupID must not be empty")
}
baseURL := nexusURL
switch nexusVersion {
case "nexus2":
@ -191,85 +165,8 @@ func getBaseURL(nexusURL, nexusVersion, repository, groupID string) (string, err
default:
return "", fmt.Errorf("unsupported Nexus version '%s', must be 'nexus2' or 'nexus3'", nexusVersion)
}
groupPath := strings.ReplaceAll(groupID, ".", "/")
baseURL += repository + "/" + groupPath + "/"
baseURL += repository + "/"
// Replace any double slashes, as nexus does not like them
baseURL = strings.ReplaceAll(baseURL, "//", "/")
return baseURL, nil
}
func getArtifactURL(baseURL, version string, artifact ArtifactDescription) string {
url := baseURL
// Generate artifact name including optional classifier
artifactName := artifact.ID + "-" + version
if len(artifact.Classifier) > 0 {
artifactName += "-" + artifact.Classifier
}
artifactName += "." + artifact.Type
url += artifact.ID + "/" + version + "/" + artifactName
// Remove any double slashes, as Nexus does not like them, and prepend protocol
url = "http://" + strings.ReplaceAll(url, "//", "/")
return url
}
func uploadFile(client piperHttp.Sender, filePath, url string) error {
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("failed to open artifact file %s: %w", filePath, err)
}
defer file.Close()
err = uploadToNexus(client, file, url)
if err != nil {
return fmt.Errorf("failed to upload artifact file %s: %w", filePath, err)
}
return nil
}
func uploadHash(client piperHttp.Sender, filePath, url string, hash hash.Hash, length int) error {
hashReader, err := generateHashReader(filePath, hash, length)
if err != nil {
return fmt.Errorf("failed to generate hash %w", err)
}
err = uploadToNexus(client, hashReader, url)
if err != nil {
return fmt.Errorf("failed to upload hash %w", err)
}
return nil
}
func uploadToNexus(client piperHttp.Sender, stream io.Reader, url string) error {
response, err := client.SendRequest(http.MethodPut, url, stream, nil, nil)
if err == nil {
log.Entry().Info("Uploaded '"+url+"', response: ", response.StatusCode)
}
return err
}
func generateHashReader(filePath string, hash hash.Hash, length int) (io.Reader, error) {
// Open file
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
// Read file and feed the hash
_, err = io.Copy(hash, file)
if err != nil {
return nil, err
}
// Get the requested number of bytes from the hash
hashInBytes := hash.Sum(nil)[:length]
// Convert the bytes to a string
hexString := hex.EncodeToString(hashInBytes)
// Finally create an io.Reader wrapping the string
return strings.NewReader(hexString), nil
}

View File

@ -1,12 +1,8 @@
package nexus
import (
"fmt"
"io"
"net/http"
"testing"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/stretchr/testify/assert"
)
@ -15,7 +11,6 @@ func TestAddArtifact(t *testing.T) {
nexusUpload := Upload{}
err := nexusUpload.AddArtifact(ArtifactDescription{
ID: "artifact.id",
Classifier: "",
Type: "pom",
File: "pom.xml",
@ -24,42 +19,14 @@ func TestAddArtifact(t *testing.T) {
assert.NoError(t, err, "Expected to add valid artifact")
assert.True(t, len(nexusUpload.artifacts) == 1)
assert.True(t, nexusUpload.artifacts[0].ID == "artifact.id")
assert.True(t, nexusUpload.artifacts[0].Classifier == "")
assert.True(t, nexusUpload.artifacts[0].Type == "pom")
assert.True(t, nexusUpload.artifacts[0].File == "pom.xml")
})
t.Run("Test missing ID", func(t *testing.T) {
nexusUpload := Upload{}
err := nexusUpload.AddArtifact(ArtifactDescription{
ID: "",
Classifier: "",
Type: "pom",
File: "pom.xml",
})
assert.Error(t, err, "Expected to fail adding invalid artifact")
assert.True(t, len(nexusUpload.artifacts) == 0)
})
t.Run("Test invalid ID", func(t *testing.T) {
nexusUpload := Upload{}
err := nexusUpload.AddArtifact(ArtifactDescription{
ID: "artifact/id",
Classifier: "",
Type: "pom",
File: "pom.xml",
})
assert.Error(t, err, "Expected to fail adding invalid artifact")
assert.True(t, len(nexusUpload.artifacts) == 0)
})
t.Run("Test missing type", func(t *testing.T) {
nexusUpload := Upload{}
err := nexusUpload.AddArtifact(ArtifactDescription{
ID: "artifact",
Classifier: "",
Type: "",
File: "pom.xml",
@ -72,7 +39,6 @@ func TestAddArtifact(t *testing.T) {
nexusUpload := Upload{}
err := nexusUpload.AddArtifact(ArtifactDescription{
ID: "artifact",
Classifier: "",
Type: "pom",
File: "",
@ -85,13 +51,11 @@ func TestAddArtifact(t *testing.T) {
nexusUpload := Upload{}
_ = nexusUpload.AddArtifact(ArtifactDescription{
ID: "blob",
Classifier: "",
Type: "pom",
File: "pom.xml",
})
err := nexusUpload.AddArtifact(ArtifactDescription{
ID: "blob",
Classifier: "",
Type: "pom",
File: "pom.xml",
@ -105,7 +69,6 @@ func TestGetArtifacts(t *testing.T) {
nexusUpload := Upload{}
err := nexusUpload.AddArtifact(ArtifactDescription{
ID: "artifact.id",
Classifier: "",
Type: "pom",
File: "pom.xml",
@ -115,245 +78,119 @@ func TestGetArtifacts(t *testing.T) {
artifacts := nexusUpload.GetArtifacts()
// Overwrite array entry in the returned array...
artifacts[0] = ArtifactDescription{
ID: "another.id",
Classifier: "",
Type: "pom",
File: "pom.xml",
Type: "jar",
File: "app.jar",
}
// ... but expect the entry in nexusUpload object to be unchanged
assert.True(t, nexusUpload.artifacts[0].ID == "artifact.id")
assert.Equal(t, "pom", nexusUpload.artifacts[0].Type)
assert.Equal(t, "pom.xml", nexusUpload.artifacts[0].File)
}
func TestGetBaseURL(t *testing.T) {
// Invalid parameters to getBaseURL() already tested via SetBaseURL() tests
// Invalid parameters to getBaseURL() already tested via SetRepoURL() tests
t.Run("Test base URL for nexus2 is sensible", func(t *testing.T) {
baseURL, err := getBaseURL("localhost:8081/nexus", "nexus2", "maven-releases", "some.group.id")
baseURL, err := getBaseURL("localhost:8081/nexus", "nexus2", "maven-releases")
assert.NoError(t, err, "Expected getBaseURL() to succeed")
assert.Equal(t, "localhost:8081/nexus/content/repositories/maven-releases/some/group/id/", baseURL)
assert.Equal(t, "localhost:8081/nexus/content/repositories/maven-releases/", baseURL)
})
t.Run("Test base URL for nexus3 is sensible", func(t *testing.T) {
baseURL, err := getBaseURL("localhost:8081", "nexus3", "maven-releases", "some.group.id")
baseURL, err := getBaseURL("localhost:8081", "nexus3", "maven-releases")
assert.NoError(t, err, "Expected getBaseURL() to succeed")
assert.Equal(t, "localhost:8081/repository/maven-releases/some/group/id/", baseURL)
assert.Equal(t, "localhost:8081/repository/maven-releases/", baseURL)
})
}
func TestSetBaseURL(t *testing.T) {
t.Run("Test no host provided", func(t *testing.T) {
nexusUpload := Upload{}
err := nexusUpload.SetBaseURL("", "nexus3", "maven-releases", "some.group.id")
assert.Error(t, err, "Expected SetBaseURL() to fail (no host)")
err := nexusUpload.SetRepoURL("", "nexus3", "maven-releases")
assert.Error(t, err, "Expected SetRepoURL() to fail (no host)")
})
t.Run("Test host wrongly includes protocol http://", func(t *testing.T) {
nexusUpload := Upload{}
err := nexusUpload.SetBaseURL("htTp://localhost:8081", "nexus3", "maven-releases", "some.group.id")
assert.Error(t, err, "Expected SetBaseURL() to fail (invalid host)")
err := nexusUpload.SetRepoURL("htTp://localhost:8081", "nexus3", "maven-releases")
assert.Error(t, err, "Expected SetRepoURL() to fail (invalid host)")
})
t.Run("Test host wrongly includes protocol https://", func(t *testing.T) {
nexusUpload := Upload{}
err := nexusUpload.SetBaseURL("htTpS://localhost:8081", "nexus3", "maven-releases", "some.group.id")
assert.Error(t, err, "Expected SetBaseURL() to fail (invalid host)")
err := nexusUpload.SetRepoURL("htTpS://localhost:8081", "nexus3", "maven-releases")
assert.Error(t, err, "Expected SetRepoURL() to fail (invalid host)")
})
t.Run("Test invalid version provided", func(t *testing.T) {
nexusUpload := Upload{}
err := nexusUpload.SetBaseURL("localhost:8081", "3", "maven-releases", "some.group.id")
assert.Error(t, err, "Expected SetBaseURL() to fail (invalid nexus version)")
err := nexusUpload.SetRepoURL("localhost:8081", "3", "maven-releases")
assert.Error(t, err, "Expected SetRepoURL() to fail (invalid nexus version)")
})
t.Run("Test no repository provided", func(t *testing.T) {
nexusUpload := Upload{}
err := nexusUpload.SetBaseURL("localhost:8081", "nexus3", "", "some.group.id")
assert.Error(t, err, "Expected SetBaseURL() to fail (no repository)")
})
t.Run("Test no group id provided", func(t *testing.T) {
nexusUpload := Upload{}
err := nexusUpload.SetBaseURL("localhost:8081", "nexus3", "maven-releases", "")
assert.Error(t, err, "Expected SetBaseURL() to fail (no groupID)")
err := nexusUpload.SetRepoURL("localhost:8081", "nexus3", "")
assert.Error(t, err, "Expected SetRepoURL() to fail (no repository)")
})
t.Run("Test no nexus version provided", func(t *testing.T) {
nexusUpload := Upload{}
err := nexusUpload.SetBaseURL("localhost:8081", "nexus1", "maven-releases", "some.group.id")
assert.Error(t, err, "Expected SetBaseURL() to fail (unsupported nexus version)")
err := nexusUpload.SetRepoURL("localhost:8081", "nexus1", "maven-releases")
assert.Error(t, err, "Expected SetRepoURL() to fail (unsupported nexus version)")
})
t.Run("Test unsupported nexus version provided", func(t *testing.T) {
nexusUpload := Upload{}
err := nexusUpload.SetBaseURL("localhost:8081", "nexus1", "maven-releases", "some.group.id")
assert.Error(t, err, "Expected SetBaseURL() to fail (unsupported nexus version)")
err := nexusUpload.SetRepoURL("localhost:8081", "nexus1", "maven-releases")
assert.Error(t, err, "Expected SetRepoURL() to fail (unsupported nexus version)")
})
}
func TestSetArtifactsVersion(t *testing.T) {
func TestSetInfo(t *testing.T) {
t.Run("Test invalid artifact version", func(t *testing.T) {
nexusUpload := Upload{}
err := nexusUpload.SetArtifactsVersion("")
assert.Error(t, err, "Expected SetArtifactsVersion() to fail (empty version)")
err := nexusUpload.SetInfo("my.group", "artifact.id", "")
assert.Error(t, err, "Expected SetInfo() to fail (empty version)")
assert.Equal(t, "", nexusUpload.groupID)
assert.Equal(t, "", nexusUpload.artifactID)
assert.Equal(t, "", nexusUpload.version)
})
t.Run("Test valid artifact version", func(t *testing.T) {
nexusUpload := Upload{}
err := nexusUpload.SetArtifactsVersion("1.0.0-SNAPSHOT")
assert.NoError(t, err, "Expected SetArtifactsVersion() to succeed")
err := nexusUpload.SetInfo("my.group", "artifact.id", "1.0.0-SNAPSHOT")
assert.NoError(t, err, "Expected SetInfo() to succeed")
})
}
type simpleHttpMock struct {
responseStatus string
responseError error
}
func (m *simpleHttpMock) SendRequest(method, url string, body io.Reader, header http.Header, cookies []*http.Cookie) (*http.Response, error) {
return &http.Response{Status: m.responseStatus}, m.responseError
}
func (m *simpleHttpMock) SetOptions(options piperhttp.ClientOptions) {
}
func TestUploadNoInit(t *testing.T) {
var mockedHttp = simpleHttpMock{
responseStatus: "200 OK",
responseError: nil,
}
t.Run("Expect that upload fails without base-URL", func(t *testing.T) {
t.Run("Test empty artifactID", func(t *testing.T) {
nexusUpload := Upload{}
err := nexusUpload.uploadArtifacts(&mockedHttp)
assert.EqualError(t, err, "the nexus.Upload needs to be configured by calling SetBaseURL() first")
err := nexusUpload.SetInfo("my.group", "", "1.0")
assert.Error(t, err, "Expected to fail setting empty artifactID")
assert.Equal(t, "", nexusUpload.groupID)
assert.Equal(t, "", nexusUpload.artifactID)
assert.Equal(t, "", nexusUpload.version)
})
t.Run("Expect that upload fails without version", func(t *testing.T) {
t.Run("Test empty groupID", func(t *testing.T) {
nexusUpload := Upload{}
_ = nexusUpload.SetBaseURL("localhost:8081", "nexus3", "maven-releases", "my.group.id")
err := nexusUpload.uploadArtifacts(&mockedHttp)
assert.EqualError(t, err, "the nexus.Upload needs to be configured by calling SetArtifactsVersion() first")
err := nexusUpload.SetInfo("", "id", "1.0")
assert.Error(t, err, "Expected to fail setting empty groupID")
assert.Equal(t, "", nexusUpload.groupID)
assert.Equal(t, "", nexusUpload.artifactID)
assert.Equal(t, "", nexusUpload.version)
})
t.Run("Expect that upload fails without artifacts", func(t *testing.T) {
t.Run("Test invalid ID", func(t *testing.T) {
nexusUpload := Upload{}
_ = nexusUpload.SetBaseURL("localhost:8081", "nexus3", "maven-releases", "my.group.id")
_ = nexusUpload.SetArtifactsVersion("1.0")
err := nexusUpload.uploadArtifacts(&mockedHttp)
assert.EqualError(t, err, "no artifacts to upload, call AddArtifact() first")
err := nexusUpload.SetInfo("my.group", "artifact/id", "1.0.0-SNAPSHOT")
assert.Error(t, err, "Expected to fail adding invalid artifact")
assert.Equal(t, "", nexusUpload.groupID)
assert.Equal(t, "", nexusUpload.artifactID)
assert.Equal(t, "", nexusUpload.version)
})
}
type request struct {
method string
url string
}
type requestReply struct {
response string
err error
}
type httpMock struct {
username string
password string
requestIndex int
requestReplies []requestReply
requests []request
}
func (m *httpMock) SendRequest(method, url string, body io.Reader, header http.Header, cookies []*http.Cookie) (*http.Response, error) {
// store the request
m.requests = append(m.requests, request{method: method, url: url})
// Return the configured response for this request's index
response := m.requestReplies[m.requestIndex].response
err := m.requestReplies[m.requestIndex].err
m.requestIndex++
return &http.Response{Status: response}, err
}
func (m *httpMock) SetOptions(options piperhttp.ClientOptions) {
m.username = options.Username
m.password = options.Password
}
func (m *httpMock) appendReply(reply requestReply) {
m.requestReplies = append(m.requestReplies, reply)
}
func createConfiguredNexusUpload() Upload {
func TestClear(t *testing.T) {
nexusUpload := Upload{}
_ = nexusUpload.SetBaseURL("localhost:8081", "nexus3", "maven-releases", "my.group.id")
_ = nexusUpload.SetArtifactsVersion("1.0")
_ = nexusUpload.AddArtifact(ArtifactDescription{ID: "artifact.id", Classifier: "", Type: "pom", File: "../../pom.xml"})
return nexusUpload
}
func TestUploadArtifacts(t *testing.T) {
t.Run("Test upload works", func(t *testing.T) {
var mockedHttp = httpMock{}
// There will be three requests, md5, sha1 and the file itself
mockedHttp.appendReply(requestReply{response: "200 OK", err: nil})
mockedHttp.appendReply(requestReply{response: "200 OK", err: nil})
mockedHttp.appendReply(requestReply{response: "200 OK", err: nil})
nexusUpload := createConfiguredNexusUpload()
err := nexusUpload.uploadArtifacts(&mockedHttp)
assert.NoError(t, err, "Expected that uploading the artifact works")
assert.Equal(t, 3, mockedHttp.requestIndex, "Expected 3 HTTP requests")
assert.Equal(t, http.MethodPut, mockedHttp.requests[0].method)
assert.Equal(t, http.MethodPut, mockedHttp.requests[1].method)
assert.Equal(t, http.MethodPut, mockedHttp.requests[2].method)
assert.Equal(t, "http://localhost:8081/repository/maven-releases/my/group/id/artifact.id/1.0/artifact.id-1.0.pom.md5",
mockedHttp.requests[0].url)
assert.Equal(t, "http://localhost:8081/repository/maven-releases/my/group/id/artifact.id/1.0/artifact.id-1.0.pom.sha1",
mockedHttp.requests[1].url)
assert.Equal(t, "http://localhost:8081/repository/maven-releases/my/group/id/artifact.id/1.0/artifact.id-1.0.pom",
mockedHttp.requests[2].url)
})
t.Run("Test upload fails at md5 hash", func(t *testing.T) {
var mockedHttp = httpMock{}
// There will be three requests, md5, sha1 and the file itself
mockedHttp.appendReply(requestReply{response: "404 Error", err: fmt.Errorf("failed")})
mockedHttp.appendReply(requestReply{response: "200 OK", err: nil})
mockedHttp.appendReply(requestReply{response: "200 OK", err: nil})
nexusUpload := createConfiguredNexusUpload()
err := nexusUpload.uploadArtifacts(&mockedHttp)
assert.Error(t, err, "Expected that uploading the artifact failed")
assert.Equal(t, 1, mockedHttp.requestIndex, "Expected only one HTTP requests")
assert.Equal(t, 1, len(nexusUpload.artifacts), "Expected the artifact to be still present in the nexusUpload")
})
t.Run("Test upload fails at sha1 hash", func(t *testing.T) {
var mockedHttp = httpMock{}
// There will be three requests, md5, sha1 and the file itself
mockedHttp.appendReply(requestReply{response: "200 OK", err: nil})
mockedHttp.appendReply(requestReply{response: "404 Error", err: fmt.Errorf("failed")})
mockedHttp.appendReply(requestReply{response: "200 OK", err: nil})
nexusUpload := createConfiguredNexusUpload()
err := nexusUpload.uploadArtifacts(&mockedHttp)
assert.Error(t, err, "Expected that uploading the artifact failed")
assert.Equal(t, 2, mockedHttp.requestIndex, "Expected only two HTTP requests")
assert.Equal(t, 1, len(nexusUpload.artifacts), "Expected the artifact to be still present in the nexusUpload")
})
t.Run("Test upload fails at file", func(t *testing.T) {
var mockedHttp = httpMock{}
// There will be three requests, md5, sha1 and the file itself
mockedHttp.appendReply(requestReply{response: "200 OK", err: nil})
mockedHttp.appendReply(requestReply{response: "200 OK", err: nil})
mockedHttp.appendReply(requestReply{response: "404 Error", err: fmt.Errorf("failed")})
nexusUpload := createConfiguredNexusUpload()
err := nexusUpload.uploadArtifacts(&mockedHttp)
assert.Error(t, err, "Expected that uploading the artifact failed")
assert.Equal(t, 3, mockedHttp.requestIndex, "Expected only three HTTP requests")
assert.Equal(t, 1, len(nexusUpload.artifacts), "Expected the artifact to be still present in the nexusUpload")
})
err := nexusUpload.AddArtifact(ArtifactDescription{
Classifier: "",
Type: "pom",
File: "pom.xml",
})
assert.NoError(t, err, "Expected to succeed adding valid artifact")
assert.Equal(t, 1, len(nexusUpload.GetArtifacts()))
nexusUpload.Clear()
assert.Equal(t, 0, len(nexusUpload.GetArtifacts()))
}

View File

@ -0,0 +1,101 @@
metadata:
name: nexusUpload
aliases:
- name: mavenExecute
deprecated: false
description: Upload artifacts to Nexus
longDescription: |
Upload build artifacts to a Nexus Repository Manager
spec:
inputs:
secrets:
- name: nexusCredentialsId
description: The technical username/password credential for accessing the nexus endpoint.
type: jenkins
params:
- name: version
type: string
description: The Nexus Repository Manager version. Currently supported are 'nexus2' and 'nexus3'.
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: false
default: nexus3
- name: url
type: string
description: URL of the nexus. The scheme part of the URL will not be considered, because only http is supported.
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: true
- name: repository
type: string
description: Name of the nexus repository.
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: true
default:
- name: groupId
type: string
description: Group ID of the artifacts. Only used in MTA projects, ignored for Maven.
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: false
- name: artifactId
type: string
description: The artifact ID used for both the .mtar and mta.yaml files deployed for MTA projects, ignored for Maven.
scope:
- PARAMETERS
- STAGES
- STEPS
- name: globalSettingsFile
type: string
description: Path to the mvn settings file that should be used as global settings file.
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
aliases:
- name: maven/globalSettingsFile
- name: m2Path
type: string
description: The path to the local .m2 directory, only used for Maven projects.
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
aliases:
- name: maven/m2Path
- name: additionalClassifiers
type: string
description: List of additional classifiers that should be deployed to nexus. Each item is a map of a type and a classifier name.
scope:
- PARAMETERS
- STAGES
- STEPS
- name: user
type: string
description: User
scope:
- PARAMETERS
- STAGES
- STEPS
- name: password
type: string
description: Password
scope:
- PARAMETERS
- STAGES
- STEPS
containers:
- name: mvn
image: maven:3.6-jdk-8
imagePullPolicy: Never

View File

@ -57,6 +57,7 @@ public class CommonStepsTest extends BasePiperTest{
'prepareDefaultValues',
'setupCommonPipelineEnvironment',
'buildSetResult',
'nexusUpload',
]
List steps = getSteps().stream()
@ -127,6 +128,7 @@ public class CommonStepsTest extends BasePiperTest{
'xsDeploy', //implementing new golang pattern without fields
'cloudFoundryDeleteService', //implementing new golang pattern without fields
'mavenExecuteStaticCodeChecks', //implementing new golang pattern without fields
'nexusUpload', //implementing new golang pattern without fields
]
@Test

View File

@ -0,0 +1,81 @@
import groovy.json.JsonSlurper
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.junit.rules.RuleChain
import util.*
import static org.hamcrest.Matchers.*
import static org.junit.Assert.assertThat
class NexusUploadTest extends BasePiperTest {
private ExpectedException exception = ExpectedException.none()
private JenkinsCredentialsRule credentialsRule = new JenkinsCredentialsRule(this)
private JenkinsReadJsonRule readJsonRule = new JenkinsReadJsonRule(this)
private JenkinsShellCallRule shellCallRule = new JenkinsShellCallRule(this)
private JenkinsStepRule stepRule = new JenkinsStepRule(this)
private JenkinsWriteFileRule writeFileRule = new JenkinsWriteFileRule(this)
private JenkinsFileExistsRule fileExistsRule = new JenkinsFileExistsRule(this, [])
//private JenkinsDockerExecuteRule dockerExecuteRule = new JenkinsDockerExecuteRule(this, [])
private List withEnvArgs = []
@Rule
public RuleChain rules = Rules
.getCommonRules(this)
.around(exception)
.around(new JenkinsReadYamlRule(this))
.around(credentialsRule)
.around(readJsonRule)
.around(shellCallRule)
.around(stepRule)
.around(writeFileRule)
.around(fileExistsRule)
// .around(dockerExecuteRule)
@Before
void init() {
helper.registerAllowedMethod('fileExists', [Map], {
return true
})
helper.registerAllowedMethod("readJSON", [Map], { m ->
if (m.file == 'nexusUpload_reports.json')
return [[target: "1234.pdf", mandatory: true]]
if (m.file == 'nexusUpload_links.json')
return []
if (m.text != null)
return new JsonSlurper().parseText(m.text)
})
helper.registerAllowedMethod("withEnv", [List, Closure], { arguments, closure ->
arguments.each {arg ->
withEnvArgs.add(arg.toString())
}
return closure()
})
// helper.registerAllowedMethod("dockerExecute", [Map, Closure], { map, closure ->
// // ignore
// })
credentialsRule.withCredentials('idOfCxCredential', "admin", "admin123")
shellCallRule.setReturnValue(
'./piper getConfig --contextConfig --stepMetadata \'.pipeline/tmp/metadata/nexusUpload.yaml\'',
'{"credentialsId": "idOfCxCredential", "verbose": false}'
)
}
@Test
void testDeployPom() {
stepRule.step.nexusUpload(
juStabUtils: utils,
jenkinsUtilsStub: jenkinsUtils,
testParam: "This is test content",
script: nullScript,
)
// asserts
assertThat(writeFileRule.files['.pipeline/tmp/metadata/nexusUpload.yaml'], containsString('name: nexusUpload'))
assertThat(withEnvArgs[0], allOf(startsWith('PIPER_parametersJSON'),
containsString('"testParam":"This is test content"')))
assertThat(shellCallRule.shell[1], is('./piper nexusUpload'))
}
}

View File

@ -164,6 +164,7 @@ class commonPipelineEnvironment implements Serializable {
[filename: '.pipeline/commonPipelineEnvironment/git/branch', property: 'gitBranch'],
[filename: '.pipeline/commonPipelineEnvironment/git/commitId', property: 'gitCommitId'],
[filename: '.pipeline/commonPipelineEnvironment/git/commitMessage', property: 'gitCommitMessage'],
[filename: '.pipeline/commonPipelineEnvironment/mtarFilePath', property: 'mtarFilePath'],
]
void writeToDisk(script) {

11
vars/nexusUpload.groovy Normal file
View File

@ -0,0 +1,11 @@
import groovy.transform.Field
@Field String STEP_NAME = getClass().getName()
@Field String METADATA_FILE = 'metadata/nexusUpload.yaml'
//Metadata maintained in file project://resources/metadata/nexusUpload.yaml
void call(Map parameters = [:]) {
List credentials = [[type: 'usernamePassword', id: 'nexusCredentialsId', env: ['PIPER_username', 'PIPER_password']]]
piperExecuteBin(parameters, STEP_NAME, METADATA_FILE, credentials)
}

View File

@ -49,6 +49,7 @@ void dockerWrapper(script, config, body) {
script: script,
dockerImage: config.dockerImage,
dockerWorkspace: config.dockerWorkspace,
dockerOptions: config.dockerOptions,
//ToDo: add additional dockerExecute parameters
) {
body()