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
2019-02-04 10:03:58 +02:00
@Field Set GENERAL_CONFIG_KEYS = [
2019-02-04 15:35:44 +02:00
/ * *
*
* /
2019-02-04 10:03:58 +02:00
'jenkinsKubernetes'
]
@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' ,
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
/ * *
* Name of the docker image that should be used . If empty , Docker is not used and the command is executed directly on the Jenkins system .
* /
2018-10-04 17:06:42 +02:00
'dockerImage' ,
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
/ * *
* 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
/ * *
* Volumes that should be mounted into the container .
* /
2018-10-04 17:06:42 +02:00
'dockerVolumeBind' ,
2019-02-06 09:48:33 +02:00
/ * *
* Set this to 'false' to bypass a docker image pull . Usefull during development process . Allows testing of images which are available in the local registry only .
* /
'dockerPullImage' ,
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-06 09:48:33 +02:00
/ * *
* Set this to 'false' to bypass a docker image pull . Usefull during development process . Allows testing of images which are available in the local registry only .
* /
'sidecarPullImage' ,
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
] )
@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS
2018-05-14 17:11:27 +02:00
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 ) {
handlePipelineStepErrors ( stepName: STEP_NAME , stepParameters: parameters ) {
2018-09-21 16:55:31 +02:00
2018-10-31 09:40:12 +02:00
final script = checkScript ( this , parameters ) ? : this
2018-09-21 16:55:31 +02:00
2018-11-05 12:24:25 +02:00
def utils = parameters ? . juStabUtils ? : new Utils ( )
2018-11-05 14:30:43 +02:00
2018-10-17 11:05:20 +02:00
Map config = ConfigurationHelper . newInstance ( this )
2018-09-07 10:08:16 +02:00
. loadStepDefaults ( )
2018-08-21 15:45:59 +02:00
. mixinGeneralConfig ( script . commonPipelineEnvironment , GENERAL_CONFIG_KEYS )
. mixinStepConfig ( script . commonPipelineEnvironment , STEP_CONFIG_KEYS )
2018-08-29 10:31:01 +02:00
. mixinStageConfig ( script . commonPipelineEnvironment , parameters . stageName ? : env . STAGE_NAME , STEP_CONFIG_KEYS )
2018-08-21 15:45:59 +02:00
. mixin ( parameters , PARAMETER_KEYS )
. use ( )
if ( isKubernetes ( ) & & config . dockerImage ) {
if ( env . POD_NAME & & isContainerDefined ( config ) ) {
container ( getContainerDefined ( config ) ) {
2018-10-04 17:06:42 +02:00
echo "[INFO][${STEP_NAME}] Executing inside a Kubernetes Container."
2018-08-21 15:45:59 +02:00
body ( )
sh "chown -R 1000:1000 ."
}
} else {
2018-10-04 17:06:42 +02:00
if ( ! config . sidecarImage ) {
dockerExecuteOnKubernetes (
script: script ,
2019-01-14 15:43:07 +02:00
containerCommand: config . containerCommand ,
containerShell: config . containerShell ,
2018-10-04 17:06:42 +02:00
dockerImage: config . dockerImage ,
2019-02-06 09:48:33 +02:00
dockerPullImage: config . dockerPullImage ,
2018-10-04 17:06:42 +02:00
dockerEnvVars: config . dockerEnvVars ,
2018-11-05 12:24:25 +02:00
dockerWorkspace: config . dockerWorkspace ,
stashContent: config . stashContent
2018-10-04 17:06:42 +02:00
) {
echo "[INFO][${STEP_NAME}] Executing inside a Kubernetes Pod"
body ( )
}
} else {
2019-03-18 15:05:42 +02:00
if ( ! config . dockerName ) {
config . dockerName = UUID . randomUUID ( ) . toString ( )
}
2018-10-04 17:06:42 +02:00
Map paramMap = [
script: script ,
containerCommands: [ : ] ,
containerEnvVars: [ : ] ,
2019-02-06 09:48:33 +02:00
containerPullImageFlags: [ : ] ,
2018-10-04 17:06:42 +02:00
containerMap: [ : ] ,
containerName: config . dockerName ,
containerPortMappings: [ : ] ,
2018-11-05 12:24:25 +02:00
containerWorkspaces: [ : ] ,
stashContent: config . stashContent
2018-10-04 17:06:42 +02:00
]
2019-03-18 15:05:42 +02:00
2018-10-04 17:06:42 +02:00
paramMap . containerCommands [ config . sidecarImage ] = ''
paramMap . containerEnvVars [ config . dockerImage ] = config . dockerEnvVars
paramMap . containerEnvVars [ config . sidecarImage ] = config . sidecarEnvVars
2019-02-06 09:48:33 +02:00
paramMap . containerPullImageFlags [ config . dockerImage ] = config . dockerPullImage
paramMap . containerPullImageFlags [ config . sidecarImage ] = config . sidecarPullImage
2018-10-04 17:06:42 +02:00
paramMap . containerMap [ config . dockerImage ] = config . dockerName
paramMap . containerMap [ config . sidecarImage ] = config . sidecarName
paramMap . containerPortMappings = config . containerPortMappings
paramMap . containerWorkspaces [ config . dockerImage ] = config . dockerWorkspace
paramMap . containerWorkspaces [ config . sidecarImage ] = ''
dockerExecuteOnKubernetes ( paramMap ) {
echo "[INFO][${STEP_NAME}] Executing inside a Kubernetes Pod with sidecar container"
2019-03-18 15:05:42 +02:00
if ( config . sidecarReadyCommand ) {
waitForSidecarReadyOnKubernetes ( config . sidecarName , config . sidecarReadyCommand )
}
2018-10-04 17:06:42 +02:00
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 )
2019-02-06 09:48:33 +02:00
if ( config . dockerPullImage ) image . pull ( )
else echo "[INFO][$STEP_NAME] Skipped pull of image '${config.dockerImage}'."
2018-10-04 17:06:42 +02:00
if ( ! config . sidecarImage ) {
image . inside ( getDockerOptions ( config . dockerEnvVars , config . dockerVolumeBind , config . dockerOptions ) ) {
body ( )
}
} else {
2018-10-24 10:13:28 +02:00
def networkName = "sidecar-${UUID.randomUUID()}"
sh "docker network create ${networkName}"
try {
def sidecarImage = docker . image ( config . sidecarImage )
2019-02-06 09:48:33 +02:00
if ( config . sidecarPullImage ) sidecarImage . pull ( )
else echo "[INFO][$STEP_NAME] Skipped pull of image '${config.sidecarImage}'."
2018-10-24 10:13:28 +02:00
config . sidecarOptions = config . sidecarOptions ? : [ ]
2019-02-06 09:48:33 +02:00
if ( config . sidecarName )
2018-10-24 10:13:28 +02:00
config . sidecarOptions . add ( "--network-alias ${config.sidecarName}" )
config . sidecarOptions . add ( "--network ${networkName}" )
2019-03-18 15:05:42 +02:00
sidecarImage . withRun ( getDockerOptions ( config . sidecarEnvVars , config . sidecarVolumeBind , config . sidecarOptions ) ) { container - >
2018-10-24 10:13:28 +02:00
config . dockerOptions = config . dockerOptions ? : [ ]
2019-02-06 09:48:33 +02:00
if ( config . dockerName )
2018-10-24 10:13:28 +02:00
config . dockerOptions . add ( "--network-alias ${config.dockerName}" )
config . dockerOptions . add ( "--network ${networkName}" )
2019-03-18 15:05:42 +02:00
if ( config . sidecarReadyCommand ) {
waitForSidecarReadyOnDocker ( container . id , config . sidecarReadyCommand )
}
2018-10-24 10:13:28 +02:00
image . inside ( getDockerOptions ( config . dockerEnvVars , config . dockerVolumeBind , config . dockerOptions ) ) {
echo "[INFO][${STEP_NAME}] Running with sidecar container."
body ( )
}
2018-10-04 17:06:42 +02:00
}
2018-10-24 10:13:28 +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 ( )
}
}
}
}
2019-03-18 15:05:42 +02:00
private waitForSidecarReadyOnDocker ( String containerId , String command ) {
String dockerCommand = "docker exec ${containerId} ${command}"
waitForSidecarReady ( dockerCommand )
}
2018-08-21 15:45:59 +02:00
2019-03-18 15:05:42 +02:00
private waitForSidecarReadyOnKubernetes ( String containerName , String command ) {
container ( name: containerName ) {
waitForSidecarReady ( command )
}
}
private waitForSidecarReady ( String command ) {
int sleepTimeInSeconds = 10
int timeoutInSeconds = 5 * 60
int maxRetries = timeoutInSeconds / sleepTimeInSeconds
int retries = 0
while ( true ) {
echo "Waiting for sidecar container"
String status = sh script: command , returnStatus: true
if ( status = = "0" ) return
if ( retries > maxRetries ) {
error ( "Timeout while waiting for sidecar container to be ready" )
}
sleep sleepTimeInSeconds
retries + +
}
}
2018-04-20 12:59:17 +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
* /
@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 ) {
for ( String k : dockerEnvVars . keySet ( ) ) {
2018-03-29 14:13:11 +02:00
options . add ( "--env ${k}=${dockerEnvVars[k].toString()}" )
2017-12-06 13:03:06 +02:00
}
}
for ( String envVar : specialEnvironments ) {
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 ) {
for ( String k : dockerVolumeBind . keySet ( ) ) {
2018-03-29 14:13:11 +02:00
options . add ( "--volume ${k}:${dockerVolumeBind[k].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 ) {
2018-08-21 15:45:59 +02:00
for ( String option : dockerOptions ) {
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
}
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 > .
* /
@NonCPS
def escapeBlanks ( def s ) {
def EQ = '='
def parts = s . split ( EQ )
if ( parts . length = = 2 ) {
parts [ 1 ] = parts [ 1 ] . replaceAll ( ' ' , '\\\\ ' )
s = parts . join ( EQ )
}
return s
}