2019-09-12 10:52:05 +02:00
import com.sap.piper.SidecarUtils
2018-09-21 16:55:31 +02:00
import static com . sap . piper . Prerequisites . checkScript
2017-12-06 13:03:06 +02:00
import com.cloudbees.groovy.cps.NonCPS
2018-08-21 15:45:59 +02:00
import com.sap.piper.ConfigurationHelper
2019-02-04 15:35:44 +02:00
import com.sap.piper.GenerateDocumentation
2018-08-21 15:45:59 +02:00
import com.sap.piper.JenkinsUtils
2018-11-05 12:24:25 +02:00
import com.sap.piper.Utils
import com.sap.piper.k8s.ContainerMap
2018-08-21 15:45:59 +02:00
import groovy.transform.Field
2018-06-07 13:58:32 +02:00
2018-11-29 10:54:05 +02:00
@Field def STEP_NAME = getClass ( ) . getName ( )
2018-08-21 15:45:59 +02:00
@Field def PLUGIN_ID_DOCKER_WORKFLOW = 'docker-workflow'
2017-12-13 11:58:22 +02:00
2021-01-13 11:48:48 +02:00
@Field Set GENERAL_CONFIG_KEYS = [
/ * *
* Set this to 'false' to bypass a docker image pull . Useful during development process . Allows testing of images which are available in the local registry only .
* /
'dockerPullImage' ,
/ * *
* Set this to 'false' to bypass a docker image pull . Useful during development process . Allows testing of images which are available in the local registry only .
* /
'sidecarPullImage'
]
2019-02-04 10:03:58 +02:00
@Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS . plus ( [
2019-02-04 15:35:44 +02:00
/ * *
* Kubernetes only:
* Allows to specify start command for container created with dockerImage parameter to overwrite Piper default ( ` /usr/ bin /tail -f / dev / null ` ) .
* /
2019-01-14 15:43:07 +02:00
'containerCommand' ,
2019-02-04 15:35:44 +02:00
/ * *
* Map which defines per docker image the port mappings , e . g . ` containerPortMappings: [ 'selenium/standalone-chrome' : [ [ name: 'selPort' , containerPort: 4444 , hostPort: 4444 ] ] ] ` .
* /
'containerPortMappings' ,
/ * *
* Kubernetes only:
* Allows to specify the shell to be used for execution of commands .
* /
2019-01-14 15:43:07 +02:00
'containerShell' ,
2020-10-02 12:56:16 +02:00
/ * *
* Kubernetes only: Allows to specify additional pod properties . For more details see step ` dockerExecuteOnKubernetes `
* /
'additionalPodProperties' ,
2019-02-04 15:35:44 +02:00
/ * *
* Environment variables to set in the container , e . g . [ http_proxy: 'proxy:8080' ] .
* /
2018-10-04 17:06:42 +02:00
'dockerEnvVars' ,
2019-02-04 15:35:44 +02:00
/ * *
2019-11-17 23:08:47 +02:00
* Name of the docker image that should be used .
* Configure with empty value to execute the command directly on the Jenkins system ( not using a container ) .
* Omit to use the default image ( cf . [ default_pipeline_environment . yml ] ( https: //github.com/SAP/jenkins-library/blob/master/resources/default_pipeline_environment.yml))
* Overwrite to use custom Docker image .
2019-02-04 15:35:44 +02:00
* /
2018-10-04 17:06:42 +02:00
'dockerImage' ,
2020-09-23 11:50:57 +02:00
/ * *
* The registry used for pulling the docker image , if left empty the default registry as defined by the ` docker - commons - plugin ` will be used .
* /
2020-11-17 15:53:09 +02:00
'dockerRegistryUrl' ,
2020-09-23 11:50:57 +02:00
/ * *
* The credentials for the docker registry . If left empty , images are pulled anonymously .
* /
2020-11-17 15:53:09 +02:00
'dockerRegistryCredentialsId' ,
2020-09-23 11:50:57 +02:00
/ * *
2020-11-17 15:53:09 +02:00
* Same as ` dockerRegistryUrl ` , but for the sidecar . If left empty , ` dockerRegistryUrl ` is used instead .
2020-09-23 11:50:57 +02:00
* /
2020-11-17 15:53:09 +02:00
'sidecarRegistryUrl' ,
2020-09-23 11:50:57 +02:00
/ * *
2020-11-17 15:53:09 +02:00
* Same as ` dockerRegistryCredentialsId ` , but for the sidecar . If left empty ` dockerRegistryCredentialsId ` is used instead .
2020-09-23 11:50:57 +02:00
* /
2020-11-17 15:53:09 +02:00
'sidecarRegistryCredentialsId' ,
2019-02-04 15:35:44 +02:00
/ * *
* Kubernetes only:
* Name of the container launching ` dockerImage ` .
* SideCar only:
* Name of the container in local network .
* /
2018-10-04 17:06:42 +02:00
'dockerName' ,
2019-02-04 15:35:44 +02:00
/ * *
2020-03-20 12:36:16 +02:00
* Docker only:
2019-02-04 15:35:44 +02:00
* Docker options to be set when starting the container ( List or String ) .
* /
2018-10-04 17:06:42 +02:00
'dockerOptions' ,
2019-02-04 15:35:44 +02:00
/ * *
2020-03-20 12:36:16 +02:00
* Docker only:
2019-02-04 15:35:44 +02:00
* Volumes that should be mounted into the container .
* /
2018-10-04 17:06:42 +02:00
'dockerVolumeBind' ,
2019-02-04 15:35:44 +02:00
/ * *
* Kubernetes only:
* Specifies a dedicated user home directory for the container which will be passed as value for environment variable ` HOME ` .
* /
'dockerWorkspace' ,
/ * *
* as ` dockerEnvVars ` for the sidecar container
* /
2018-10-04 17:06:42 +02:00
'sidecarEnvVars' ,
2019-02-04 15:35:44 +02:00
/ * *
* as ` dockerImage ` for the sidecar container
* /
2018-10-04 17:06:42 +02:00
'sidecarImage' ,
2019-02-04 15:35:44 +02:00
/ * *
* as ` dockerName ` for the sidecar container
* /
2018-10-24 10:13:28 +02:00
'sidecarName' ,
2019-02-04 15:35:44 +02:00
/ * *
* as ` dockerOptions ` for the sidecar container
* /
2018-10-04 17:06:42 +02:00
'sidecarOptions' ,
2019-02-04 15:35:44 +02:00
/ * *
* as ` dockerVolumeBind ` for the sidecar container
* /
2018-11-05 12:24:25 +02:00
'sidecarVolumeBind' ,
2019-02-04 15:35:44 +02:00
/ * *
* as ` dockerWorkspace ` for the sidecar container
* /
'sidecarWorkspace' ,
2019-03-18 15:05:42 +02:00
/ * *
* Command executed inside the container which returns exit code 0 when the container is ready to be used .
* /
'sidecarReadyCommand' ,
2019-02-04 15:35:44 +02:00
/ * *
* Specific stashes that should be considered for the step execution .
* /
2018-11-05 12:24:25 +02:00
'stashContent'
2019-02-04 10:03:58 +02:00
] )
2020-06-08 17:08:05 +02:00
@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS . plus ( [
/ * *
* In the Kubernetes case the workspace is only available to the respective Jenkins slave but not to the containers running inside the pod . < br / >
* This flag controls whether the stashing does * not * use the default exclude patterns in addition to the patterns provided in ` stashExcludes ` .
* @possibleValues ` true ` , ` false `
* /
'stashNoDefaultExcludes' ,
] )
2018-05-14 17:11:27 +02:00
2020-11-17 18:44:19 +02:00
@Field Map CONFIG_KEY_COMPATIBILITY = [
dockerRegistryCredentialsId: 'dockerRegistryCredentials' ,
sidecarRegistryCredentialsId: 'dockerSidecarRegistryCredentials' ,
]
2019-02-04 15:35:44 +02:00
/ * *
* Executes a closure inside a docker container with the specified docker image .
* The workspace is mounted into the docker image .
* Proxy environment variables defined on the Jenkins machine are also available in the Docker container .
* /
@GenerateDocumentation
2018-08-21 15:45:59 +02:00
void call ( Map parameters = [ : ] , body ) {
2019-04-08 20:10:54 +02:00
handlePipelineStepErrors ( stepName: STEP_NAME , stepParameters: parameters , failOnError: true ) {
2018-09-21 16:55:31 +02:00
2018-10-31 09:40:12 +02:00
final script = checkScript ( this , parameters ) ? : this
2020-08-26 15:32:58 +02:00
def utils = parameters . juStabUtils ? : new Utils ( )
String stageName = parameters . stageName ? : env . STAGE_NAME
2018-11-05 14:30:43 +02:00
2018-10-17 11:05:20 +02:00
Map config = ConfigurationHelper . newInstance ( this )
2020-08-26 15:32:58 +02:00
. loadStepDefaults ( [ : ] , stageName )
2020-11-17 18:44:19 +02:00
. mixinGeneralConfig ( script . commonPipelineEnvironment , GENERAL_CONFIG_KEYS , CONFIG_KEY_COMPATIBILITY )
. mixinStepConfig ( script . commonPipelineEnvironment , STEP_CONFIG_KEYS , CONFIG_KEY_COMPATIBILITY )
. mixinStageConfig ( script . commonPipelineEnvironment , stageName , STEP_CONFIG_KEYS , CONFIG_KEY_COMPATIBILITY )
. mixin ( parameters , PARAMETER_KEYS , CONFIG_KEY_COMPATIBILITY )
2018-08-21 15:45:59 +02:00
. use ( )
2019-03-26 18:06:34 +02:00
2020-09-23 11:50:57 +02:00
config = ConfigurationHelper . newInstance ( this , config )
2020-11-17 15:53:09 +02:00
. addIfEmpty ( 'sidecarRegistryUrl' , config . dockerRegistryUrl )
. addIfEmpty ( 'sidecarRegistryCredentialsId' , config . dockerRegistryCredentialsId )
2020-09-23 11:50:57 +02:00
. use ( )
2019-09-12 10:52:05 +02:00
SidecarUtils sidecarUtils = new SidecarUtils ( script )
2019-03-26 18:06:34 +02:00
new Utils ( ) . pushToSWA ( [
step: STEP_NAME ,
stepParamKey1: 'scriptMissing' ,
stepParam1: parameters ? . script = = null ,
stepParamKey2: 'kubernetes' ,
stepParam2: isKubernetes ( )
] , config )
2018-08-21 15:45:59 +02:00
if ( isKubernetes ( ) & & config . dockerImage ) {
2019-08-16 17:05:18 +02:00
List dockerEnvVars = [ ]
config . dockerEnvVars ? . each { key , value - >
dockerEnvVars < < "$key=$value"
}
2018-08-21 15:45:59 +02:00
if ( env . POD_NAME & & isContainerDefined ( config ) ) {
container ( getContainerDefined ( config ) ) {
2019-08-16 17:05:18 +02:00
withEnv ( dockerEnvVars ) {
echo "[INFO][${STEP_NAME}] Executing inside a Kubernetes Container."
body ( )
sh "chown -R 1000:1000 ."
}
2018-08-21 15:45:59 +02:00
}
} else {
2019-09-12 10:52:05 +02:00
if ( ! config . dockerName ) {
config . dockerName = UUID . randomUUID ( ) . toString ( )
}
2020-09-30 13:44:40 +02:00
def dockerExecuteOnKubernetesParams = [
script: script ,
2020-10-02 12:56:16 +02:00
additionalPodProperties: config . additionalPodProperties ,
2020-09-30 13:44:40 +02:00
containerName: config . dockerName ,
containerCommand: config . containerCommand ,
containerShell: config . containerShell ,
dockerImage: config . dockerImage ,
dockerPullImage: config . dockerPullImage ,
dockerEnvVars: config . dockerEnvVars ,
dockerWorkspace: config . dockerWorkspace ,
stashContent: config . stashContent ,
stashNoDefaultExcludes: config . stashNoDefaultExcludes ,
]
if ( config . sidecarImage ) {
dockerExecuteOnKubernetesParams + = [
2019-09-12 10:52:05 +02:00
containerPortMappings: config . containerPortMappings ,
sidecarName: parameters . sidecarName ,
sidecarImage: parameters . sidecarImage ,
sidecarPullImage: parameters . sidecarPullImage ,
sidecarReadyCommand: parameters . sidecarReadyCommand ,
2020-09-30 13:44:40 +02:00
sidecarEnvVars: parameters . sidecarEnvVars ,
]
}
dockerExecuteOnKubernetes ( dockerExecuteOnKubernetesParams ) {
echo "[INFO][${STEP_NAME}] Executing inside a Kubernetes Pod"
body ( )
2018-08-21 15:45:59 +02:00
}
}
} else {
boolean executeInsideDocker = true
if ( ! JenkinsUtils . isPluginActive ( PLUGIN_ID_DOCKER_WORKFLOW ) ) {
echo "[WARNING][${STEP_NAME}] Docker not supported. Plugin '${PLUGIN_ID_DOCKER_WORKFLOW}' is not installed or not active. Configured docker image '${config.dockerImage}' will not be used."
executeInsideDocker = false
2017-12-13 11:58:22 +02:00
}
2018-01-15 14:42:57 +02:00
returnCode = sh script: 'docker ps -q > /dev/null' , returnStatus: true
2018-08-21 15:45:59 +02:00
if ( returnCode ! = 0 ) {
echo "[WARNING][$STEP_NAME] Cannot connect to docker daemon (command 'docker ps' did not return with '0'). Configured docker image '${config.dockerImage}' will not be used."
executeInsideDocker = false
2018-01-15 14:42:57 +02:00
}
2018-08-21 15:45:59 +02:00
if ( executeInsideDocker & & config . dockerImage ) {
2018-11-05 12:24:25 +02:00
utils . unstashAll ( config . stashContent )
2018-08-21 15:45:59 +02:00
def image = docker . image ( config . dockerImage )
2020-11-17 15:53:09 +02:00
pullWrapper ( config . dockerPullImage , image , config . dockerRegistryUrl , config . dockerRegistryCredentialsId ) {
if ( ! config . sidecarImage ) {
image . inside ( getDockerOptions ( config . dockerEnvVars , config . dockerVolumeBind , config . dockerOptions ) ) {
body ( )
}
} else {
def networkName = "sidecar-${UUID.randomUUID()}"
sh "docker network create ${networkName}"
try {
def sidecarImage = docker . image ( config . sidecarImage )
pullWrapper ( config . sidecarPullImage , sidecarImage , config . sidecarRegistryUrl , config . sidecarRegistryCredentialsId ) {
config . sidecarOptions = config . sidecarOptions ? : [ ]
if ( config . sidecarName )
config . sidecarOptions . add ( "--network-alias ${config.sidecarName}" )
config . sidecarOptions . add ( "--network ${networkName}" )
sidecarImage . withRun ( getDockerOptions ( config . sidecarEnvVars , config . sidecarVolumeBind , config . sidecarOptions ) ) { container - >
config . dockerOptions = config . dockerOptions ? : [ ]
if ( config . dockerName )
config . dockerOptions . add ( "--network-alias ${config.dockerName}" )
config . dockerOptions . add ( "--network ${networkName}" )
if ( config . sidecarReadyCommand ) {
sidecarUtils . waitForSidecarReadyOnDocker ( container . id , config . sidecarReadyCommand )
}
image . inside ( getDockerOptions ( config . dockerEnvVars , config . dockerVolumeBind , config . dockerOptions ) ) {
echo "[INFO][${STEP_NAME}] Running with sidecar container."
body ( )
}
}
2018-10-24 10:13:28 +02:00
}
2020-11-17 15:53:09 +02:00
} finally {
sh "docker network remove ${networkName}"
2018-10-04 17:06:42 +02:00
}
}
2018-08-21 15:45:59 +02:00
}
} else {
echo "[INFO][${STEP_NAME}] Running on local environment."
2017-12-06 13:03:06 +02:00
body ( )
}
}
}
}
2020-11-17 15:53:09 +02:00
void pullWrapper ( boolean pullImage , def dockerImage , String dockerRegistryUrl , String dockerCredentialsId , Closure body ) {
if ( ! pullImage ) {
echo "[INFO][$STEP_NAME] Skipped pull of image '$dockerImage'."
body ( )
return
}
2020-09-23 11:50:57 +02:00
if ( dockerCredentialsId ) {
2020-11-17 15:53:09 +02:00
// docker registry can be provided empty and will default to 'https://index.docker.io/v1/' in this case.
docker . withRegistry ( dockerRegistryUrl ? : '' , dockerCredentialsId ) {
dockerImage . pull ( )
body ( )
}
} else if ( dockerRegistryUrl ) {
docker . withRegistry ( dockerRegistryUrl ) {
2020-09-23 11:50:57 +02:00
dockerImage . pull ( )
2020-11-17 15:53:09 +02:00
body ( )
2020-09-23 11:50:57 +02:00
}
} else {
dockerImage . pull ( )
2020-11-17 15:53:09 +02:00
body ( )
2020-09-23 11:50:57 +02:00
}
}
2019-02-04 11:53:20 +02:00
/ *
2017-12-06 13:03:06 +02:00
* Returns a string with docker options containing
* environment variables ( if set ) .
* Possible to extend with further options .
* @param dockerEnvVars Map with environment variables
* /
2019-09-12 10:52:05 +02:00
2017-12-06 13:03:06 +02:00
@NonCPS
private getDockerOptions ( Map dockerEnvVars , Map dockerVolumeBind , def dockerOptions ) {
2018-06-08 11:55:38 +02:00
def specialEnvironments = [
'http_proxy' ,
'https_proxy' ,
'no_proxy' ,
'HTTP_PROXY' ,
'HTTPS_PROXY' ,
'NO_PROXY'
]
2018-03-29 14:13:11 +02:00
def options = [ ]
2017-12-06 13:03:06 +02:00
if ( dockerEnvVars ) {
2020-03-11 11:05:03 +02:00
dockerEnvVars . each { String k , v - >
options . add ( "--env ${k}=${v.toString()}" )
2017-12-06 13:03:06 +02:00
}
}
2020-03-11 11:05:03 +02:00
specialEnvironments . each { String envVar - >
2017-12-06 13:03:06 +02:00
if ( dockerEnvVars = = null | | ! dockerEnvVars . containsKey ( envVar ) ) {
2018-03-29 14:13:11 +02:00
options . add ( "--env ${envVar}" )
2017-12-06 13:03:06 +02:00
}
}
if ( dockerVolumeBind ) {
2020-03-11 11:05:03 +02:00
dockerVolumeBind . each { String k , v - >
options . add ( "--volume ${k}:${v.toString()}" )
2017-12-06 13:03:06 +02:00
}
}
2018-08-21 15:45:59 +02:00
if ( dockerOptions ) {
if ( dockerOptions instanceof CharSequence ) {
2019-01-18 15:43:57 +02:00
dockerOptions = [ dockerOptions ]
}
if ( dockerOptions instanceof List ) {
2020-03-11 11:05:03 +02:00
dockerOptions . each { String option - >
2019-01-18 14:40:15 +02:00
options < < escapeBlanks ( option )
2018-08-21 15:45:59 +02:00
}
} else {
throw new IllegalArgumentException ( "Unexpected type for dockerOptions. Expected was either a list or a string. Actual type was: '${dockerOptions.getClass()}'" )
2018-03-29 14:13:11 +02:00
}
2017-12-06 13:03:06 +02:00
}
2018-03-29 14:13:11 +02:00
return options . join ( ' ' )
2017-12-06 13:03:06 +02:00
}
2018-08-21 15:45:59 +02:00
boolean isContainerDefined ( config ) {
Map containerMap = ContainerMap . instance . getMap ( )
if ( ! containerMap . containsKey ( env . POD_NAME ) ) {
return false
}
2020-07-29 19:51:27 +02:00
if ( env . SIDECAR_IMAGE ! = config . sidecarImage ) {
// If a sidecar image has been configured for the current stage,
// then piperStageWrapper will have set the env.SIDECAR_IMAGE variable.
// If the current step overrides the stage's sidecar image,
// then a new Pod needs to be spawned.
return false
}
2018-08-21 15:45:59 +02:00
return containerMap . get ( env . POD_NAME ) . containsKey ( config . dockerImage )
}
def getContainerDefined ( config ) {
return ContainerMap . instance . getMap ( ) . get ( env . POD_NAME ) . get ( config . dockerImage ) . toLowerCase ( )
}
boolean isKubernetes ( ) {
return Boolean . valueOf ( env . ON_K8S )
}
2019-01-18 14:40:15 +02:00
2019-02-04 11:53:20 +02:00
/ *
2019-01-18 14:40:15 +02:00
* Escapes blanks for values in key / value pairs
* E . g . < code > description = Lorem ipsum < / code > is
* changed to < code > description = Lorem \ ipsum < / code > .
* /
2019-09-12 10:52:05 +02:00
2019-01-18 14:40:15 +02:00
@NonCPS
def escapeBlanks ( def s ) {
2019-09-12 10:52:05 +02:00
def EQ = '='
def parts = s . split ( EQ )
2019-01-18 14:40:15 +02:00
2019-09-12 10:52:05 +02:00
if ( parts . length = = 2 ) {
parts [ 1 ] = parts [ 1 ] . replaceAll ( ' ' , '\\\\ ' )
2019-01-18 14:40:15 +02:00
s = parts . join ( EQ )
}
return s
}