2020-03-20 19:20:52 +02:00
package cmd
import (
"fmt"
2020-11-10 18:14:55 +02:00
piperhttp "github.com/SAP/jenkins-library/pkg/http"
2020-03-20 19:20:52 +02:00
"github.com/pkg/errors"
2020-11-10 18:14:55 +02:00
"io"
"net/http"
2020-03-20 19:20:52 +02:00
"os"
"path/filepath"
2020-04-03 21:32:38 +02:00
"strings"
2020-03-20 19:20:52 +02:00
2020-04-11 12:56:44 +02:00
b64 "encoding/base64"
2020-03-20 19:20:52 +02:00
"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 {
2020-11-10 18:14:55 +02:00
Stdout ( out io . Writer )
Stderr ( err io . Writer )
SetEnv ( env [ ] string )
RunExecutable ( e string , p ... string ) error
2020-06-15 09:47:33 +02:00
FileExists ( path string ) ( bool , error )
FileRead ( path string ) ( [ ] byte , error )
FileWrite ( path string , content [ ] byte , perm os . FileMode ) error
FileRemove ( path string ) error
DirExists ( path string ) ( bool , error )
Glob ( pattern string ) ( matches [ ] string , err error )
2020-11-10 18:14:55 +02:00
Copy ( src , dest string ) ( int64 , error )
MkdirAll ( path string , perm os . FileMode ) error
DownloadFile ( url , filename string , header http . Header , cookies [ ] * http . Cookie ) error
2020-06-15 09:47:33 +02:00
UsesMta ( ) bool
UsesMaven ( ) bool
UsesNpm ( ) bool
2020-03-20 19:20:52 +02:00
getEnvParameter ( path , name string ) string
2020-06-11 14:02:54 +02:00
evaluate ( options * maven . EvaluateOptions , expression string ) ( string , error )
2020-03-20 19:20:52 +02:00
}
type utilsBundle struct {
2020-06-15 09:47:33 +02:00
* piperutils . ProjectStructure
* piperutils . Files
2020-11-10 18:14:55 +02:00
* command . Command
* piperhttp . Client
2020-03-20 19:20:52 +02:00
}
func newUtilsBundle ( ) * utilsBundle {
2020-11-10 18:14:55 +02:00
utils := utilsBundle {
2020-06-15 09:47:33 +02:00
ProjectStructure : & piperutils . ProjectStructure { } ,
Files : & piperutils . Files { } ,
2020-11-10 18:14:55 +02:00
Command : & command . Command { } ,
Client : & piperhttp . Client { } ,
2020-03-20 19:20:52 +02:00
}
2020-11-10 18:14:55 +02:00
utils . Stdout ( log . Writer ( ) )
utils . Stderr ( log . Writer ( ) )
return & utils
2020-03-20 19:20:52 +02:00
}
2020-06-15 09:47:33 +02:00
func ( u * utilsBundle ) FileWrite ( filePath string , content [ ] byte , perm os . FileMode ) error {
2020-03-20 19:20:52 +02:00
parent := filepath . Dir ( filePath )
if parent != "" {
2020-06-15 09:47:33 +02:00
err := u . Files . MkdirAll ( parent , 0775 )
2020-03-20 19:20:52 +02:00
if err != nil {
return err
}
}
2020-06-15 09:47:33 +02:00
return u . Files . FileWrite ( filePath , content , perm )
2020-04-11 12:56:44 +02:00
}
2020-03-20 19:20:52 +02:00
func ( u * utilsBundle ) getEnvParameter ( path , name string ) string {
return piperenv . GetParameter ( path , name )
}
2020-06-11 14:02:54 +02:00
func ( u * utilsBundle ) evaluate ( options * maven . EvaluateOptions , expression string ) ( string , error ) {
2020-11-10 18:14:55 +02:00
return maven . Evaluate ( options , expression , u )
2020-03-20 19:20:52 +02:00
}
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 {
2020-04-11 12:56:44 +02:00
performMavenUpload := len ( options . MavenRepository ) > 0
performNpmUpload := len ( options . NpmRepository ) > 0
2020-11-11 17:32:23 +02:00
if ! performMavenUpload && ! performNpmUpload {
2021-03-10 16:06:42 +02:00
if options . Format == "" {
return fmt . Errorf ( "none of the parameters 'mavenRepository' and 'npmRepository' are configured, or 'format' should be set if the 'url' already contains the repository ID" )
}
if options . Format == "maven" {
performMavenUpload = true
} else if options . Format == "npm" {
performNpmUpload = true
}
2020-11-11 17:32:23 +02:00
}
2020-04-11 12:56:44 +02:00
err := uploader . SetRepoURL ( options . Url , options . Version , options . MavenRepository , options . NpmRepository )
2020-03-20 19:20:52 +02:00
if err != nil {
return err
}
2020-04-11 12:56:44 +02:00
2020-06-15 09:47:33 +02:00
if utils . UsesNpm ( ) && performNpmUpload {
2020-04-11 12:56:44 +02:00
log . Entry ( ) . Info ( "NPM project structure detected" )
err = uploadNpmArtifacts ( utils , uploader , options )
2020-03-20 19:20:52 +02:00
} else {
2020-04-11 12:56:44 +02:00
log . Entry ( ) . Info ( "Skipping npm upload because either no package json was found or NpmRepository option is not provided." )
2020-03-20 19:20:52 +02:00
}
2020-04-11 12:56:44 +02:00
if err != nil {
return err
}
if performMavenUpload {
2020-06-15 09:47:33 +02:00
if utils . UsesMta ( ) {
2020-04-11 12:56:44 +02:00
log . Entry ( ) . Info ( "MTA project structure detected" )
return uploadMTA ( utils , uploader , options )
2020-06-15 09:47:33 +02:00
} else if utils . UsesMaven ( ) {
2020-04-11 12:56:44 +02:00
log . Entry ( ) . Info ( "Maven project structure detected" )
return uploadMaven ( utils , uploader , options )
}
} else {
log . Entry ( ) . Info ( "Skipping maven and mta upload because mavenRepository option is not provided." )
}
return nil
}
func uploadNpmArtifacts ( utils nexusUploadUtils , uploader nexus . Uploader , options * nexusUploadOptions ) error {
2021-03-10 16:06:42 +02:00
environment := [ ] string { "npm_config_registry=" + uploader . GetNexusURLProtocol ( ) + "://" + uploader . GetNpmRepoURL ( ) , "npm_config_email=project-piper@no-reply.com" }
2020-09-16 11:33:03 +02:00
if options . Username != "" && options . Password != "" {
auth := b64 . StdEncoding . EncodeToString ( [ ] byte ( options . Username + ":" + options . Password ) )
2020-04-11 12:56:44 +02:00
environment = append ( environment , "npm_config__auth=" + auth )
} else {
log . Entry ( ) . Info ( "No credentials provided for npm upload, trying to upload anonymously." )
}
2020-11-10 18:14:55 +02:00
utils . SetEnv ( environment )
err := utils . RunExecutable ( "npm" , "publish" )
2020-04-11 12:56:44 +02:00
return err
2020-03-20 19:20:52 +02:00
}
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
2020-06-15 09:47:33 +02:00
exists , _ := utils . FileExists ( "mta.yaml" )
2020-03-20 19:20:52 +02:00
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"
}
2020-03-31 15:16:18 +02:00
mtaInfo , err := getInfoFromMtaFile ( utils , mtaPath )
2020-03-20 19:20:52 +02:00
if err == nil {
2020-03-31 15:16:18 +02:00
if options . ArtifactID != "" {
mtaInfo . ID = options . ArtifactID
}
err = uploader . SetInfo ( options . GroupID , mtaInfo . ID , mtaInfo . Version )
2020-03-20 19:20:52 +02:00
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" `
}
2020-03-31 15:16:18 +02:00
func getInfoFromMtaFile ( utils nexusUploadUtils , filePath string ) ( * mtaYaml , error ) {
2020-06-15 09:47:33 +02:00
mtaYamlContent , err := utils . FileRead ( filePath )
2020-03-20 19:20:52 +02:00
if err != nil {
2020-03-31 15:16:18 +02:00
return nil , fmt . Errorf ( "could not read from required project descriptor file '%s'" ,
2020-03-20 19:20:52 +02:00
filePath )
}
2020-03-31 15:16:18 +02:00
return getInfoFromMtaYaml ( mtaYamlContent , filePath )
2020-03-20 19:20:52 +02:00
}
2020-03-31 15:16:18 +02:00
func getInfoFromMtaYaml ( mtaYamlContent [ ] byte , filePath string ) ( * mtaYaml , error ) {
2020-03-20 19:20:52 +02:00
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).
2020-03-31 15:16:18 +02:00
return nil , fmt . Errorf ( "failed to parse contents of the project descriptor file '%s'" ,
2020-03-20 19:20:52 +02:00
filePath )
}
2020-03-31 15:16:18 +02:00
return & mtaYaml , nil
2020-03-20 19:20:52 +02:00
}
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 ) {
2020-09-16 11:33:03 +02:00
if options . Username == "" || options . Password == "" {
2020-03-20 19:20:52 +02:00
return "" , nil
}
2020-06-15 09:47:33 +02:00
err := utils . FileWrite ( settingsPath , [ ] byte ( nexusMavenSettings ) , os . ModePerm )
2020-03-20 19:20:52 +02:00
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" )
2020-11-10 18:14:55 +02:00
utils . SetEnv ( [ ] string { "NEXUS_username=" + options . Username , "NEXUS_password=" + options . Password } )
2020-03-20 19:20:52 +02:00
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
}
2020-04-03 21:32:38 +02:00
const deployGoal = "org.apache.maven.plugins:maven-deploy-plugin:2.8.2:deploy-file"
2020-03-20 19:20:52 +02:00
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
2021-03-10 16:06:42 +02:00
defines = append ( defines , "-Durl=" + uploader . GetNexusURLProtocol ( ) + "://" + uploader . GetMavenRepoURL ( ) )
2020-03-20 19:20:52 +02:00
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 != "" {
2020-06-15 09:47:33 +02:00
defer func ( ) { _ = utils . FileRemove ( settingsFile ) } ( )
2020-03-20 19:20:52 +02:00
}
// 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 )
}
}
2020-11-10 18:14:55 +02:00
err = uploadArtifactsBundle ( d , generatePOM , mavenOptions , utils )
2020-03-20 19:20:52 +02:00
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 ,
2020-11-10 18:14:55 +02:00
utils nexusUploadUtils ) error {
2020-03-20 19:20:52 +02:00
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 ... )
2020-11-10 18:14:55 +02:00
_ , err := maven . Execute ( & mavenOptions , utils )
2020-03-20 19:20:52 +02:00
return err
}
func addArtifact ( utils nexusUploadUtils , uploader nexus . Uploader , filePath , classifier , fileType string ) error {
2020-06-15 09:47:33 +02:00
exists , _ := utils . FileExists ( filePath )
2020-03-20 19:20:52 +02:00
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 {
2020-06-15 09:47:33 +02:00
pomFiles , _ := utils . Glob ( "**/pom.xml" )
2020-04-03 21:32:38 +02:00
if len ( pomFiles ) == 0 {
return errPomNotFound
2020-03-20 19:20:52 +02:00
}
2020-04-03 21:32:38 +02:00
for _ , pomFile := range pomFiles {
parentDir := filepath . Dir ( pomFile )
if parentDir == "integration-tests" || parentDir == "unit-tests" {
continue
}
err := uploadMavenArtifacts ( utils , uploader , options , parentDir , filepath . Join ( parentDir , "target" ) )
if err != nil {
return err
}
2020-03-20 19:20:52 +02:00
}
2020-04-03 21:32:38 +02:00
return nil
2020-03-20 19:20:52 +02:00
}
func uploadMavenArtifacts ( utils nexusUploadUtils , uploader nexus . Uploader , options * nexusUploadOptions ,
2020-04-03 21:32:38 +02:00
pomPath , targetFolder string ) error {
2020-03-20 19:20:52 +02:00
pomFile := composeFilePath ( pomPath , "pom" , "xml" )
2020-04-03 21:32:38 +02:00
2020-06-11 14:02:54 +02:00
evaluateOptions := & maven . EvaluateOptions {
PomPath : pomFile ,
GlobalSettingsFile : options . GlobalSettingsFile ,
M2Path : options . M2Path ,
}
packaging , _ := utils . evaluate ( evaluateOptions , "project.packaging" )
2020-04-03 21:32:38 +02:00
if packaging == "" {
packaging = "jar"
}
if packaging != "pom" {
// Ignore this module if there is no 'target' folder
2020-06-15 09:47:33 +02:00
hasTarget , _ := utils . DirExists ( targetFolder )
2020-04-03 21:32:38 +02:00
if ! hasTarget {
log . Entry ( ) . Warnf ( "Ignoring module '%s' as it has no 'target' folder" , pomPath )
return nil
}
2020-03-20 19:20:52 +02:00
}
2020-06-11 14:02:54 +02:00
groupID , _ := utils . evaluate ( evaluateOptions , "project.groupId" )
2020-03-20 19:20:52 +02:00
if groupID == "" {
groupID = options . GroupID
}
2020-06-11 14:02:54 +02:00
artifactID , err := utils . evaluate ( evaluateOptions , "project.artifactId" )
2020-03-20 19:20:52 +02:00
var artifactsVersion string
if err == nil {
2020-06-11 14:02:54 +02:00
artifactsVersion , err = utils . evaluate ( evaluateOptions , "project.version" )
2020-03-20 19:20:52 +02:00
}
if err == nil {
err = uploader . SetInfo ( groupID , artifactID , artifactsVersion )
}
var finalBuildName string
if err == nil {
2020-06-11 14:02:54 +02:00
finalBuildName , _ = utils . evaluate ( evaluateOptions , "project.build.finalName" )
2020-03-20 19:20:52 +02:00
if finalBuildName == "" {
2020-04-03 21:32:38 +02:00
// Fallback to composing final build name, see http://maven.apache.org/pom.html#BaseBuild_Element
finalBuildName = artifactID + "-" + artifactsVersion
2020-03-20 19:20:52 +02:00
}
}
if err == nil {
err = addArtifact ( utils , uploader , pomFile , "" , "pom" )
}
2020-04-03 21:32:38 +02:00
if err == nil && packaging != "pom" {
err = addMavenTargetArtifacts ( utils , uploader , pomFile , targetFolder , finalBuildName , packaging )
2020-03-20 19:20:52 +02:00
}
if err == nil {
err = uploadArtifacts ( utils , uploader , options , true )
}
return err
}
2020-04-03 21:32:38 +02:00
func addMavenTargetArtifacts ( utils nexusUploadUtils , uploader nexus . Uploader , pomFile , targetFolder , finalBuildName , packaging string ) error {
fileTypes := [ ] string { packaging }
if packaging != "jar" {
// Try to find additional artifacts with a classifier
fileTypes = append ( fileTypes , "jar" )
2020-03-20 19:20:52 +02:00
}
2020-04-03 21:32:38 +02:00
for _ , fileType := range fileTypes {
pattern := targetFolder + "/*." + fileType
2020-06-15 09:47:33 +02:00
matches , _ := utils . Glob ( pattern )
2020-04-03 21:32:38 +02:00
if len ( matches ) == 0 && fileType == packaging {
return fmt . Errorf ( "target artifact not found for packaging '%s'" , packaging )
2020-03-20 19:20:52 +02:00
}
2020-04-03 21:32:38 +02:00
log . Entry ( ) . Debugf ( "Glob matches for %s: %s" , pattern , strings . Join ( matches , ", " ) )
prefix := filepath . Join ( targetFolder , finalBuildName ) + "-"
suffix := "." + fileType
for _ , filename := range matches {
classifier := ""
temp := filename
if strings . HasPrefix ( temp , prefix ) && strings . HasSuffix ( temp , suffix ) {
temp = strings . TrimPrefix ( temp , prefix )
temp = strings . TrimSuffix ( temp , suffix )
classifier = temp
}
err := addArtifact ( utils , uploader , filename , classifier , fileType )
if err != nil {
return err
}
2020-03-20 19:20:52 +02:00
}
}
return nil
}
func composeFilePath ( folder , name , extension string ) string {
fileName := name + "." + extension
return filepath . Join ( folder , fileName )
}