2020-04-03 16:34:40 +02:00
package cmd
import (
"bytes"
"fmt"
2020-11-10 18:14:55 +02:00
"io"
netHttp "net/http"
2020-04-03 16:34:40 +02:00
"os"
"strings"
"text/template"
"time"
2021-05-10 17:44:28 +02:00
piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/piperutils"
2020-04-03 16:34:40 +02:00
"github.com/SAP/jenkins-library/pkg/command"
2021-02-15 13:34:19 +02:00
gitUtils "github.com/SAP/jenkins-library/pkg/git"
2020-04-03 16:34:40 +02:00
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/telemetry"
"github.com/SAP/jenkins-library/pkg/versioning"
"github.com/pkg/errors"
"github.com/go-git/go-git/v5"
gitConfig "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
)
type gitRepository interface {
2020-05-07 11:21:29 +02:00
CommitObject ( plumbing . Hash ) ( * object . Commit , error )
2020-04-03 16:34:40 +02:00
CreateTag ( string , plumbing . Hash , * git . CreateTagOptions ) ( * plumbing . Reference , error )
CreateRemote ( * gitConfig . RemoteConfig ) ( * git . Remote , error )
DeleteRemote ( string ) error
Push ( * git . PushOptions ) error
Remote ( string ) ( * git . Remote , error )
ResolveRevision ( plumbing . Revision ) ( * plumbing . Hash , error )
Worktree ( ) ( * git . Worktree , error )
}
type gitWorktree interface {
Checkout ( * git . CheckoutOptions ) error
Commit ( string , * git . CommitOptions ) ( plumbing . Hash , error )
}
func getGitWorktree ( repository gitRepository ) ( gitWorktree , error ) {
return repository . Worktree ( )
}
2020-11-10 18:14:55 +02:00
type artifactPrepareVersionUtils interface {
Stdout ( out io . Writer )
Stderr ( err io . Writer )
RunExecutable ( e string , p ... string ) error
DownloadFile ( url , filename string , header netHttp . Header , cookies [ ] * netHttp . Cookie ) error
Glob ( pattern string ) ( matches [ ] string , err error )
FileExists ( filename string ) ( bool , error )
Copy ( src , dest string ) ( int64 , error )
MkdirAll ( path string , perm os . FileMode ) error
2021-06-01 09:24:36 +02:00
FileWrite ( path string , content [ ] byte , perm os . FileMode ) error
FileRead ( path string ) ( [ ] byte , error )
2020-11-10 18:14:55 +02:00
}
type artifactPrepareVersionUtilsBundle struct {
* command . Command
* piperutils . Files
* piperhttp . Client
}
func newArtifactPrepareVersionUtilsBundle ( ) artifactPrepareVersionUtils {
utils := artifactPrepareVersionUtilsBundle {
Command : & command . Command { } ,
Files : & piperutils . Files { } ,
Client : & piperhttp . Client { } ,
}
utils . Stdout ( log . Writer ( ) )
utils . Stderr ( log . Writer ( ) )
return & utils
}
2020-04-03 16:34:40 +02:00
func artifactPrepareVersion ( config artifactPrepareVersionOptions , telemetryData * telemetry . CustomData , commonPipelineEnvironment * artifactPrepareVersionCommonPipelineEnvironment ) {
2020-11-10 18:14:55 +02:00
utils := newArtifactPrepareVersionUtilsBundle ( )
2020-04-03 16:34:40 +02:00
// open local .git repository
repository , err := openGit ( )
if err != nil {
log . Entry ( ) . WithError ( err ) . Fatal ( "git repository required - none available" )
}
2020-11-10 18:14:55 +02:00
err = runArtifactPrepareVersion ( & config , telemetryData , commonPipelineEnvironment , nil , utils , repository , getGitWorktree )
2020-04-03 16:34:40 +02:00
if err != nil {
log . Entry ( ) . WithError ( err ) . Fatal ( "artifactPrepareVersion failed" )
}
}
var sshAgentAuth = ssh . NewSSHAgentAuth
2020-11-10 18:14:55 +02:00
func runArtifactPrepareVersion ( config * artifactPrepareVersionOptions , telemetryData * telemetry . CustomData , commonPipelineEnvironment * artifactPrepareVersionCommonPipelineEnvironment , artifact versioning . Artifact , utils artifactPrepareVersionUtils , repository gitRepository , getWorktree func ( gitRepository ) ( gitWorktree , error ) ) error {
2020-04-03 16:34:40 +02:00
telemetryData . Custom1Label = "buildTool"
telemetryData . Custom1 = config . BuildTool
2020-04-15 13:12:43 +02:00
telemetryData . Custom2Label = "filePath"
telemetryData . Custom2 = config . FilePath
2020-04-03 16:34:40 +02:00
// Options for artifact
artifactOpts := versioning . Options {
GlobalSettingsFile : config . GlobalSettingsFile ,
M2Path : config . M2Path ,
ProjectSettingsFile : config . ProjectSettingsFile ,
2020-04-24 20:52:16 +02:00
VersionField : config . CustomVersionField ,
2020-04-15 13:12:43 +02:00
VersionSection : config . CustomVersionSection ,
2020-04-24 20:52:16 +02:00
VersioningScheme : config . CustomVersioningScheme ,
2020-05-06 22:07:27 +02:00
VersionSource : config . DockerVersionSource ,
2020-04-30 17:32:18 +02:00
}
2020-04-03 16:34:40 +02:00
var err error
if artifact == nil {
2020-11-10 18:14:55 +02:00
artifact , err = versioning . GetArtifact ( config . BuildTool , config . FilePath , & artifactOpts , utils )
2020-04-03 16:34:40 +02:00
if err != nil {
2020-07-31 14:55:22 +02:00
log . SetErrorCategory ( log . ErrorConfiguration )
2020-04-03 16:34:40 +02:00
return errors . Wrap ( err , "failed to retrieve artifact" )
}
}
versioningType := config . VersioningType
// support former groovy versioning template and translate into new options
if len ( config . VersioningTemplate ) > 0 {
versioningType , _ , config . IncludeCommitID = templateCompatibility ( config . VersioningTemplate )
}
version , err := artifact . GetVersion ( )
if err != nil {
2020-07-31 14:55:22 +02:00
log . SetErrorCategory ( log . ErrorConfiguration )
2020-04-03 16:34:40 +02:00
return errors . Wrap ( err , "failed to retrieve version" )
}
log . Entry ( ) . Infof ( "Version before automatic versioning: %v" , version )
2020-05-07 11:21:29 +02:00
gitCommit , gitCommitMessage , err := getGitCommitID ( repository )
2020-04-03 16:34:40 +02:00
if err != nil {
2020-07-31 14:55:22 +02:00
log . SetErrorCategory ( log . ErrorConfiguration )
2020-04-03 16:34:40 +02:00
return err
}
gitCommitID := gitCommit . String ( )
2021-05-20 13:11:57 +02:00
commonPipelineEnvironment . git . headCommitID = gitCommitID
2020-04-03 16:34:40 +02:00
newVersion := version
2020-04-07 13:27:36 +02:00
if versioningType == "cloud" || versioningType == "cloud_noTag" {
2020-04-03 16:34:40 +02:00
versioningTempl , err := versioningTemplate ( artifact . VersioningScheme ( ) )
if err != nil {
2020-07-31 14:55:22 +02:00
log . SetErrorCategory ( log . ErrorConfiguration )
2020-04-03 16:34:40 +02:00
return errors . Wrapf ( err , "failed to get versioning template for scheme '%v'" , artifact . VersioningScheme ( ) )
}
now := time . Now ( )
2020-05-06 22:07:27 +02:00
newVersion , err = calculateNewVersion ( versioningTempl , version , gitCommitID , config . IncludeCommitID , config . ShortCommitID , config . UnixTimestamp , now )
2020-04-03 16:34:40 +02:00
if err != nil {
return errors . Wrap ( err , "failed to calculate new version" )
}
worktree , err := getWorktree ( repository )
if err != nil {
2020-07-31 14:55:22 +02:00
log . SetErrorCategory ( log . ErrorConfiguration )
2020-04-03 16:34:40 +02:00
return errors . Wrap ( err , "failed to retrieve git worktree" )
}
// opening repository does not seem to consider already existing files properly
// behavior in case we do not run initializeWorktree:
// git.Add(".") will add the complete workspace instead of only changed files
err = initializeWorktree ( gitCommit , worktree )
if err != nil {
return err
}
// only update version in build descriptor if required in order to save prossing time (e.g. maven case)
if newVersion != version {
err = artifact . SetVersion ( newVersion )
if err != nil {
2020-07-31 14:55:22 +02:00
log . SetErrorCategory ( log . ErrorConfiguration )
2020-04-03 16:34:40 +02:00
return errors . Wrap ( err , "failed to write version" )
}
}
//ToDo: what about closure in current Groovy step. Discard the possibility or provide extension mechanism?
2020-04-07 13:27:36 +02:00
if versioningType == "cloud" {
// commit changes and push to repository (including new version tag)
gitCommitID , err = pushChanges ( config , newVersion , repository , worktree , now )
if err != nil {
2021-05-10 17:44:28 +02:00
if strings . Contains ( fmt . Sprint ( err ) , "reference already exists" ) {
log . SetErrorCategory ( log . ErrorCustom )
}
2020-04-07 13:27:36 +02:00
return errors . Wrapf ( err , "failed to push changes for version '%v'" , newVersion )
}
2020-04-03 16:34:40 +02:00
}
}
log . Entry ( ) . Infof ( "New version: '%v'" , newVersion )
2021-05-20 13:11:57 +02:00
commonPipelineEnvironment . git . commitID = gitCommitID // this commitID changes and is not necessarily the HEAD commitID
2020-04-03 16:34:40 +02:00
commonPipelineEnvironment . artifactVersion = newVersion
2020-05-28 10:05:22 +02:00
commonPipelineEnvironment . originalArtifactVersion = version
2020-05-07 11:21:29 +02:00
commonPipelineEnvironment . git . commitMessage = gitCommitMessage
2020-04-03 16:34:40 +02:00
return nil
}
func openGit ( ) ( gitRepository , error ) {
workdir , _ := os . Getwd ( )
2021-02-15 13:34:19 +02:00
return gitUtils . PlainOpen ( workdir )
2020-04-03 16:34:40 +02:00
}
2020-05-07 11:21:29 +02:00
func getGitCommitID ( repository gitRepository ) ( plumbing . Hash , string , error ) {
2020-04-03 16:34:40 +02:00
commitID , err := repository . ResolveRevision ( plumbing . Revision ( "HEAD" ) )
if err != nil {
2020-05-07 11:21:29 +02:00
return plumbing . Hash { } , "" , errors . Wrap ( err , "failed to retrieve git commit ID" )
2020-04-03 16:34:40 +02:00
}
2020-05-07 11:21:29 +02:00
// ToDo not too elegant to retrieve the commit message here, must be refactored sooner than later
// but to quickly address https://github.com/SAP/jenkins-library/pull/1515 let's revive this
commitObject , err := repository . CommitObject ( * commitID )
if err != nil {
return * commitID , "" , errors . Wrap ( err , "failed to retrieve git commit message" )
}
return * commitID , commitObject . Message , nil
2020-04-03 16:34:40 +02:00
}
func versioningTemplate ( scheme string ) ( string , error ) {
// generally: timestamp acts as build number providing a proper order
switch scheme {
2020-05-06 22:07:27 +02:00
case "docker" :
// from Docker documentation:
// A tag name must be valid ASCII and may contain lowercase and uppercase letters, digits, underscores, periods and dashes.
// A tag name may not start with a period or a dash and may contain a maximum of 128 characters.
return "{{.Version}}{{if .Timestamp}}-{{.Timestamp}}{{if .CommitID}}-{{.CommitID}}{{end}}{{end}}" , nil
2020-04-03 16:34:40 +02:00
case "maven" :
// according to https://www.mojohaus.org/versions-maven-plugin/version-rules.html
return "{{.Version}}{{if .Timestamp}}-{{.Timestamp}}{{if .CommitID}}_{{.CommitID}}{{end}}{{end}}" , nil
case "pep440" :
// according to https://www.python.org/dev/peps/pep-0440/
return "{{.Version}}{{if .Timestamp}}.{{.Timestamp}}{{if .CommitID}}+{{.CommitID}}{{end}}{{end}}" , nil
case "semver2" :
// according to https://semver.org/spec/v2.0.0.html
return "{{.Version}}{{if .Timestamp}}-{{.Timestamp}}{{if .CommitID}}+{{.CommitID}}{{end}}{{end}}" , nil
}
return "" , fmt . Errorf ( "versioning scheme '%v' not supported" , scheme )
}
2020-05-06 22:07:27 +02:00
func calculateNewVersion ( versioningTemplate , currentVersion , commitID string , includeCommitID , shortCommitID , unixTimestamp bool , t time . Time ) ( string , error ) {
2020-04-03 16:34:40 +02:00
tmpl , err := template . New ( "version" ) . Parse ( versioningTemplate )
if err != nil {
return "" , errors . Wrapf ( err , "failed to create version template: %v" , versioningTemplate )
}
2020-05-06 22:07:27 +02:00
timestamp := t . Format ( "20060102150405" )
if unixTimestamp {
timestamp = fmt . Sprint ( t . Unix ( ) )
}
2020-04-03 16:34:40 +02:00
buf := new ( bytes . Buffer )
versionParts := struct {
Version string
Timestamp string
CommitID string
} {
Version : currentVersion ,
2020-05-06 22:07:27 +02:00
Timestamp : timestamp ,
2020-04-03 16:34:40 +02:00
}
if includeCommitID {
versionParts . CommitID = commitID
2020-05-06 22:07:27 +02:00
if shortCommitID {
versionParts . CommitID = commitID [ 0 : 7 ]
}
2020-04-03 16:34:40 +02:00
}
err = tmpl . Execute ( buf , versionParts )
if err != nil {
return "" , errors . Wrapf ( err , "failed to execute versioning template: %v" , versioningTemplate )
}
newVersion := buf . String ( )
if len ( newVersion ) == 0 {
return "" , fmt . Errorf ( "failed calculate version, new version is '%v'" , newVersion )
}
return buf . String ( ) , nil
}
func initializeWorktree ( gitCommit plumbing . Hash , worktree gitWorktree ) error {
// checkout current revision in order to work on that
err := worktree . Checkout ( & git . CheckoutOptions { Hash : gitCommit , Keep : true } )
if err != nil {
return errors . Wrap ( err , "failed to initialize worktree" )
}
return nil
}
func pushChanges ( config * artifactPrepareVersionOptions , newVersion string , repository gitRepository , worktree gitWorktree , t time . Time ) ( string , error ) {
var commitID string
2020-04-08 09:28:03 +02:00
commit , err := addAndCommit ( config , worktree , newVersion , t )
2020-04-03 16:34:40 +02:00
if err != nil {
return commit . String ( ) , err
}
commitID = commit . String ( )
tag := fmt . Sprintf ( "%v%v" , config . TagPrefix , newVersion )
_ , err = repository . CreateTag ( tag , commit , nil )
if err != nil {
return commitID , err
}
ref := gitConfig . RefSpec ( fmt . Sprintf ( "refs/tags/%v:refs/tags/%v" , tag , tag ) )
pushOptions := git . PushOptions {
RefSpecs : [ ] gitConfig . RefSpec { gitConfig . RefSpec ( ref ) } ,
}
currentRemoteOrigin , err := repository . Remote ( "origin" )
if err != nil {
return commitID , errors . Wrap ( err , "failed to retrieve current remote origin" )
}
var updatedRemoteOrigin * git . Remote
urls := originUrls ( repository )
if len ( urls ) == 0 {
2020-07-31 14:55:22 +02:00
log . SetErrorCategory ( log . ErrorConfiguration )
2020-04-03 16:34:40 +02:00
return commitID , fmt . Errorf ( "no remote url maintained" )
}
if strings . HasPrefix ( urls [ 0 ] , "http" ) {
if len ( config . Username ) == 0 || len ( config . Password ) == 0 {
// handling compatibility: try to use ssh in case no credentials are available
log . Entry ( ) . Info ( "git username/password missing - switching to ssh" )
remoteURL := convertHTTPToSSHURL ( urls [ 0 ] )
// update remote origin url to point to ssh url instead of http(s) url
err = repository . DeleteRemote ( "origin" )
if err != nil {
return commitID , errors . Wrap ( err , "failed to update remote origin - remove" )
}
updatedRemoteOrigin , err = repository . CreateRemote ( & gitConfig . RemoteConfig { Name : "origin" , URLs : [ ] string { remoteURL } } )
if err != nil {
return commitID , errors . Wrap ( err , "failed to update remote origin - create" )
}
pushOptions . Auth , err = sshAgentAuth ( "git" )
if err != nil {
2020-07-31 14:55:22 +02:00
log . SetErrorCategory ( log . ErrorConfiguration )
2020-04-03 16:34:40 +02:00
return commitID , errors . Wrap ( err , "failed to retrieve ssh authentication" )
}
log . Entry ( ) . Infof ( "using remote '%v'" , remoteURL )
} else {
pushOptions . Auth = & http . BasicAuth { Username : config . Username , Password : config . Password }
}
} else {
pushOptions . Auth , err = sshAgentAuth ( "git" )
if err != nil {
2020-07-31 14:55:22 +02:00
log . SetErrorCategory ( log . ErrorConfiguration )
2020-04-03 16:34:40 +02:00
return commitID , errors . Wrap ( err , "failed to retrieve ssh authentication" )
}
}
err = repository . Push ( & pushOptions )
if err != nil {
2020-08-06 11:13:19 +02:00
errText := fmt . Sprint ( err )
switch {
case strings . Contains ( errText , "ssh: handshake failed" ) :
log . SetErrorCategory ( log . ErrorConfiguration )
case strings . Contains ( errText , "Permission" ) :
log . SetErrorCategory ( log . ErrorConfiguration )
2020-08-19 20:20:00 +02:00
case strings . Contains ( errText , "authorization failed" ) :
log . SetErrorCategory ( log . ErrorConfiguration )
case strings . Contains ( errText , "authentication required" ) :
log . SetErrorCategory ( log . ErrorConfiguration )
2020-09-21 14:51:02 +02:00
case strings . Contains ( errText , "knownhosts:" ) :
2020-08-06 11:13:19 +02:00
err = errors . Wrap ( err , "known_hosts file seems invalid" )
log . SetErrorCategory ( log . ErrorConfiguration )
case strings . Contains ( errText , "unable to find any valid known_hosts file" ) :
log . SetErrorCategory ( log . ErrorConfiguration )
case strings . Contains ( errText , "connection timed out" ) :
log . SetErrorCategory ( log . ErrorInfrastructure )
}
2020-04-03 16:34:40 +02:00
return commitID , err
}
if updatedRemoteOrigin != currentRemoteOrigin {
err = repository . DeleteRemote ( "origin" )
if err != nil {
return commitID , errors . Wrap ( err , "failed to restore remote origin - remove" )
}
_ , err := repository . CreateRemote ( currentRemoteOrigin . Config ( ) )
if err != nil {
return commitID , errors . Wrap ( err , "failed to restore remote origin - create" )
}
}
return commitID , nil
}
2020-04-08 09:28:03 +02:00
func addAndCommit ( config * artifactPrepareVersionOptions , worktree gitWorktree , newVersion string , t time . Time ) ( plumbing . Hash , error ) {
2020-04-03 16:34:40 +02:00
//maybe more options are required: https://github.com/go-git/go-git/blob/master/_examples/commit/main.go
2020-05-14 17:20:13 +02:00
commit , err := worktree . Commit ( fmt . Sprintf ( "update version %v" , newVersion ) , & git . CommitOptions { All : true , Author : & object . Signature { Name : config . CommitUserName , When : t } } )
2020-04-03 16:34:40 +02:00
if err != nil {
return commit , errors . Wrap ( err , "failed to commit new version" )
}
return commit , nil
}
func originUrls ( repository gitRepository ) [ ] string {
remote , err := repository . Remote ( "origin" )
if err != nil || remote == nil {
return [ ] string { }
}
return remote . Config ( ) . URLs
}
func convertHTTPToSSHURL ( url string ) string {
sshURL := strings . Replace ( url , "https://" , "git@" , 1 )
return strings . Replace ( sshURL , "/" , ":" , 1 )
}
func templateCompatibility ( groovyTemplate string ) ( versioningType string , useTimestamp bool , useCommitID bool ) {
useTimestamp = strings . Contains ( groovyTemplate , "${timestamp}" )
useCommitID = strings . Contains ( groovyTemplate , "${commitId" )
versioningType = "library"
if useTimestamp {
versioningType = "cloud"
}
return
}