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
2018-08-21 15:45:59 +02:00
import com.sap.piper.ConfigurationHelper
2019-02-08 13:20:45 +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
2018-08-21 15:45:59 +02:00
import com.sap.piper.k8s.SystemEnv
2019-03-20 11:07:37 +02:00
import com.sap.piper.JsonUtils
2018-08-21 15:45:59 +02:00
import groovy.transform.Field
import hudson.AbortException
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_KUBERNETES = 'kubernetes'
2019-02-08 13:20:45 +02:00
2019-02-04 10:03:58 +02:00
@Field Set GENERAL_CONFIG_KEYS = [
2020-02-04 09:02:38 +02:00
/ * *
* Define settings used by the Jenkins Kuberenetes plugin .
* /
2019-06-26 08:38:47 +02:00
'jenkinsKubernetes' ,
/ * *
* Print more detailed information into the log .
* @possibleValues ` true ` , ` false `
* /
'verbose'
2019-02-04 10:03:58 +02:00
]
@Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS . plus ( [
2019-02-08 13:20:45 +02:00
/ * *
* Allows to specify start command for container created with dockerImage parameter to overwrite Piper default ( ` /usr/ bin /tail -f / dev / null ` ) .
* /
'containerCommand' ,
/ * *
* Specifies start command for containers to overwrite Piper default ( ` /usr/ bin /tail -f / dev / null ` ) .
* If container 's defaultstart command should be used provide empty string like: `[' selenium / standalone - chrome ': ' ' ] ` .
* /
'containerCommands' ,
/ * *
* Specifies environment variables per container . If not provided ` dockerEnvVars ` will be used .
* /
'containerEnvVars' ,
/ * *
* A map of docker image to the name of the container . The pod will be created with all the images from this map and they are labled based on the value field of each map entry .
2019-10-25 17:49:54 +02:00
* Example: ` [ 'maven:3.5-jdk-8-alpine' : 'mavenExecute' , 'selenium/standalone-chrome' : 'selenium' , 'famiko/jmeter-base' : 'checkJMeter' , 'ppiper/cf-cli' : 'cloudfoundry' ] `
2019-02-08 13:20:45 +02:00
* /
'containerMap' ,
/ * *
* Optional configuration in combination with containerMap to define the container where the commands should be executed in .
* /
'containerName' ,
/ * *
* Map which defines per docker image the port mappings , e . g . ` containerPortMappings: [ 'selenium/standalone-chrome' : [ [ name: 'selPort' , containerPort: 4444 , hostPort: 4444 ] ] ] ` .
* /
'containerPortMappings' ,
/ * *
* Specifies the pullImage flag per container .
* /
'containerPullImageFlags' ,
/ * *
* Allows to specify the shell to be executed for container with containerName .
* /
'containerShell' ,
/ * *
* Specifies a dedicated user home directory per container which will be passed as value for environment variable ` HOME ` . If not provided ` dockerWorkspace ` will be used .
* /
'containerWorkspaces' ,
/ * *
* Environment variables to set in the container , e . g . [ http_proxy: 'proxy:8080' ] .
* /
'dockerEnvVars' ,
/ * *
* Name of the docker image that should be used . If empty , Docker is not used .
* /
2018-10-04 17:06:42 +02:00
'dockerImage' ,
2019-02-08 13:20:45 +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 .
* /
2019-02-06 09:48:33 +02:00
'dockerPullImage' ,
2019-02-08 13:20:45 +02:00
/ * *
* Specifies a dedicated user home directory for the container which will be passed as value for environment variable ` HOME ` .
* /
2018-10-04 17:06:42 +02:00
'dockerWorkspace' ,
2019-09-12 10:52:05 +02:00
/ * *
* as ` dockerImage ` for the sidecar container
* /
'sidecarImage' ,
/ * *
* SideCar only:
* Name of the container in local network .
* /
'sidecarName' ,
/ * *
* 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' ,
/ * *
* Command executed inside the container which returns exit code 0 when the container is ready to be used .
* /
'sidecarReadyCommand' ,
/ * *
* as ` dockerEnvVars ` for the sidecar container
* /
'sidecarEnvVars' ,
/ * *
* as ` dockerWorkspace ` for the sidecar container
* /
'sidecarWorkspace' ,
/ * *
* as ` dockerVolumeBind ` for the sidecar container
* /
'sidecarVolumeBind' ,
/ * *
* as ` dockerOptions ` for the sidecar container
* /
'sidecarOptions' ,
2019-06-19 12:26:16 +02:00
/** Defines the Kubernetes nodeSelector as per [https://github.com/jenkinsci/kubernetes-plugin](https://github.com/jenkinsci/kubernetes-plugin).*/
'nodeSelector' ,
2019-04-02 14:23:19 +02:00
/ * *
* Kubernetes Security Context used for the pod .
* Can be used to specify uid and fsGroup .
* See: https: //kubernetes.io/docs/tasks/configure-pod-container/security-context/
* /
'securityContext' ,
2019-02-08 13:20:45 +02:00
/ * *
* Specific stashes that should be considered for the step execution .
* /
2019-02-04 10:03:58 +02:00
'stashContent' ,
2019-02-08 13:20:45 +02:00
/ * *
2019-08-14 16:44:12 +02:00
* 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 configuration defines exclude pattern for stashing from Jenkins workspace to working directory in container and back .
* Following excludes can be set:
2019-02-08 13:20:45 +02:00
*
2019-08-14 16:44:12 +02:00
* * ` workspace ` : Pattern for stashing towards container
* * ` stashBack ` : Pattern for bringing data from container back to Jenkins workspace . If not set: defaults to setting for ` workspace ` .
2019-02-08 13:20:45 +02:00
* /
2019-02-04 10:03:58 +02:00
'stashExcludes' ,
2019-02-08 13:20:45 +02:00
/ * *
2019-08-14 16:44:12 +02:00
* 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 configuration defines include pattern for stashing from Jenkins workspace to working directory in container and back .
* Following includes can be set:
2019-02-08 13:20:45 +02:00
*
2019-08-14 16:44:12 +02:00
* * ` workspace ` : Pattern for stashing towards container
* * ` stashBack ` : Pattern for bringing data from container back to Jenkins workspace . If not set: defaults to setting for ` workspace ` .
2019-02-08 13:20:45 +02:00
* /
2019-04-02 14:23:19 +02:00
'stashIncludes'
2019-02-04 10:03:58 +02:00
] )
@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS . minus ( [
'stashIncludes' ,
'stashExcludes'
] )
2018-08-21 15:45:59 +02:00
2019-02-08 13:20:45 +02:00
/ * *
* Executes a closure inside a container in a kubernetes pod .
* Proxy environment variables defined on the Jenkins machine are also available in the container .
2019-07-17 12:01:24 +02:00
*
* By default jnlp agent defined for kubernetes - plugin will be used ( see https: //github.com/jenkinsci/kubernetes-plugin#pipeline-support).
*
* It is possible to define a custom jnlp agent image by
*
* 1 . Defining the jnlp image via environment variable JENKINS_JNLP_IMAGE in the Kubernetes landscape
* 2 . Defining the image via config ( ` jenkinsKubernetes . jnlpAgent ` )
*
* Option 1 will take precedence over option 2 .
2019-02-08 13:20:45 +02:00
* /
@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
2018-09-21 16:55:31 +02:00
2018-08-21 15:45:59 +02:00
if ( ! JenkinsUtils . isPluginActive ( PLUGIN_ID_KUBERNETES ) ) {
error ( "[ERROR][${STEP_NAME}] not supported. Plugin '${PLUGIN_ID_KUBERNETES}' is not installed or not active." )
}
2018-11-05 12:24:25 +02:00
def utils = parameters ? . juStabUtils ? : new Utils ( )
2018-08-21 15:45:59 +02:00
2018-10-17 11:05:20 +02:00
ConfigurationHelper configHelper = 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 )
. addIfEmpty ( 'uniqueId' , UUID . randomUUID ( ) . toString ( ) )
2018-10-08 11:54:13 +02:00
Map config = configHelper . use ( )
2018-08-21 15:45:59 +02:00
2019-03-26 18:06:34 +02:00
new Utils ( ) . pushToSWA ( [
2019-09-12 10:52:05 +02:00
step : STEP_NAME ,
2019-03-26 18:06:34 +02:00
stepParamKey1: 'scriptMissing' ,
2019-09-12 10:52:05 +02:00
stepParam1 : parameters ? . script = = null
2019-03-26 18:06:34 +02:00
] , config )
2019-10-23 14:31:49 +02:00
if ( ! config . containerMap ) {
2018-10-08 11:54:13 +02:00
configHelper . withMandatoryProperty ( 'dockerImage' )
config . containerName = 'container-exec'
2019-08-21 15:04:20 +02:00
config . containerMap = [ ( config . get ( 'dockerImage' ) ) : config . containerName ]
config . containerCommands = config . containerCommand ? [ ( config . get ( 'dockerImage' ) ) : config . containerCommand ] : null
2018-08-21 15:45:59 +02:00
}
2019-09-12 10:52:05 +02:00
executeOnPod ( config , utils , body , script )
2018-08-21 15:45:59 +02:00
}
}
def getOptions ( config ) {
2019-03-20 11:07:37 +02:00
def namespace = config . jenkinsKubernetes . namespace
def options = [
2019-09-12 10:52:05 +02:00
name : 'dynamic-agent-' + config . uniqueId ,
label: config . uniqueId ,
yaml : generatePodSpec ( config )
2019-03-20 11:07:37 +02:00
]
if ( namespace ) {
options . namespace = namespace
}
2019-06-19 12:26:16 +02:00
if ( config . nodeSelector ) {
options . nodeSelector = config . nodeSelector
}
2019-06-26 08:38:47 +02:00
if ( ! config . verbose ) {
options . showRawYaml = false
}
2019-03-20 11:07:37 +02:00
return options
2018-08-21 15:45:59 +02:00
}
2019-09-12 10:52:05 +02:00
void executeOnPod ( Map config , utils , Closure body , Script script ) {
2018-08-21 15:45:59 +02:00
/ *
* There could be exceptions thrown by
- The podTemplate
- The container method
- The body
* We use nested exception handling in this case .
2018-10-08 11:54:13 +02:00
* In the first 2 cases , the 'container' stash is not created because the inner try / finally is not reached .
2018-10-05 10:51:01 +02:00
* However , the workspace has not been modified and don ' t need to be restored .
* In case third case , we need to create the 'container' stash to bring the modified content back to the host .
2018-08-21 15:45:59 +02:00
* /
try {
2019-09-12 10:52:05 +02:00
SidecarUtils sidecarUtils = new SidecarUtils ( script )
2019-04-03 13:31:07 +02:00
def stashContent = config . stashContent
2019-09-12 10:52:05 +02:00
if ( config . containerName & & stashContent . isEmpty ( ) ) {
2019-04-03 13:31:07 +02:00
stashContent = [ stashWorkspace ( config , 'workspace' ) ]
2018-11-05 12:24:25 +02:00
}
2018-08-21 15:45:59 +02:00
podTemplate ( getOptions ( config ) ) {
node ( config . uniqueId ) {
2019-09-12 10:52:05 +02:00
if ( config . sidecarReadyCommand ) {
sidecarUtils . waitForSidecarReadyOnKubernetes ( config . sidecarName , config . sidecarReadyCommand )
}
2018-10-08 11:54:13 +02:00
if ( config . containerName ) {
2019-01-08 20:44:28 +02:00
Map containerParams = [ name: config . containerName ]
if ( config . containerShell ) {
containerParams . shell = config . containerShell
}
2019-01-31 10:39:13 +02:00
echo "ContainerConfig: ${containerParams}"
2019-09-12 10:52:05 +02:00
container ( containerParams ) {
2018-10-08 11:54:13 +02:00
try {
2019-04-03 13:31:07 +02:00
utils . unstashAll ( stashContent )
2019-12-17 16:10:57 +02:00
echo "invalidate stash workspace-${config.uniqueId}"
stash name: "workspace-${config.uniqueId}" , excludes: '**/*' , allowEmpty: true
2018-10-08 11:54:13 +02:00
body ( )
} finally {
2019-12-18 15:53:38 +02:00
stashWorkspace ( config , 'container' , true , true )
2018-10-08 11:54:13 +02:00
}
2018-10-05 10:51:01 +02:00
}
2018-10-08 11:54:13 +02:00
} else {
body ( )
2018-08-21 15:45:59 +02:00
}
}
}
} finally {
2018-10-08 11:54:13 +02:00
if ( config . containerName )
unstashWorkspace ( config , 'container' )
2018-08-21 15:45:59 +02:00
}
}
2019-03-20 11:07:37 +02:00
private String generatePodSpec ( Map config ) {
def containers = getContainerList ( config )
def podSpec = [
apiVersion: "v1" ,
2019-09-12 10:52:05 +02:00
kind : "Pod" ,
metadata : [
2019-03-20 11:07:37 +02:00
lables: config . uniqueId
] ,
2019-09-12 10:52:05 +02:00
spec : [
2019-03-20 11:07:37 +02:00
containers: containers
]
]
podSpec . spec . securityContext = getSecurityContext ( config )
2019-03-22 13:11:20 +02:00
return new JsonUtils ( ) . groovyObjectToPrettyJsonString ( podSpec )
2019-03-20 11:07:37 +02:00
}
2019-08-14 16:44:12 +02:00
private String stashWorkspace ( config , prefix , boolean chown = false , boolean stashBack = false ) {
2018-11-05 12:24:25 +02:00
def stashName = "${prefix}-${config.uniqueId}"
2018-08-21 15:45:59 +02:00
try {
2019-09-12 10:52:05 +02:00
if ( chown ) {
2019-03-20 11:07:37 +02:00
def securityContext = getSecurityContext ( config )
def runAsUser = securityContext ? . runAsUser ? : 1000
def fsGroup = securityContext ? . fsGroup ? : 1000
2019-09-12 10:52:05 +02:00
sh "" " # ! $ { config . containerShell ? : '/bin/sh' }
2019-03-20 11:07:37 +02:00
chown - R $ { runAsUser } : $ { fsGroup } . "" "
2019-01-31 10:39:13 +02:00
}
2019-08-14 16:44:12 +02:00
def includes , excludes
2019-09-12 10:52:05 +02:00
if ( stashBack ) {
2019-08-14 16:44:12 +02:00
includes = config . stashIncludes . stashBack ? : config . stashIncludes . workspace
excludes = config . stashExcludes . stashBack ? : config . stashExcludes . workspace
} else {
includes = config . stashIncludes . workspace
excludes = config . stashExcludes . workspace
}
2018-08-21 15:45:59 +02:00
stash (
2018-11-05 12:24:25 +02:00
name: stashName ,
2019-08-14 16:44:12 +02:00
includes: includes ,
excludes: excludes
2018-08-21 15:45:59 +02:00
)
2019-08-14 16:44:12 +02:00
//inactive due to negative side-effects, we may require a dedicated git stash to be used
//useDefaultExcludes: false)
2018-11-05 12:24:25 +02:00
return stashName
2018-08-21 15:45:59 +02:00
} catch ( AbortException | IOException e ) {
echo "${e.getMessage()}"
}
2019-12-18 15:53:38 +02:00
return null
2018-08-21 15:45:59 +02:00
}
2019-03-20 11:07:37 +02:00
private Map getSecurityContext ( Map config ) {
return config . securityContext ? : config . jenkinsKubernetes . securityContext ? : [ : ]
}
2018-08-21 15:45:59 +02:00
private void unstashWorkspace ( config , prefix ) {
try {
unstash "${prefix}-${config.uniqueId}"
2019-12-17 16:10:57 +02:00
echo "invalidate stash ${prefix}-${config.uniqueId}"
stash name: "${prefix}-${config.uniqueId}" , excludes: '**/*' , allowEmpty: true
2018-08-21 15:45:59 +02:00
} catch ( AbortException | IOException e ) {
echo "${e.getMessage()}"
}
}
private List getContainerList ( config ) {
2019-07-17 12:01:24 +02:00
//If no custom jnlp agent provided as default jnlp agent (jenkins/jnlp-slave) as defined in the plugin, see https://github.com/jenkinsci/kubernetes-plugin#pipeline-support
def result = [ ]
//allow definition of jnlp image via environment variable JENKINS_JNLP_IMAGE in the Kubernetes landscape or via config as fallback
if ( env . JENKINS_JNLP_IMAGE | | config . jenkinsKubernetes . jnlpAgent ) {
result . push ( [
2019-09-12 10:52:05 +02:00
name : 'jnlp' ,
2019-07-17 12:01:24 +02:00
image: env . JENKINS_JNLP_IMAGE ? : config . jenkinsKubernetes . jnlpAgent
] )
}
2018-08-21 15:45:59 +02:00
config . containerMap . each { imageName , containerName - >
2019-02-06 09:48:33 +02:00
def containerPullImage = config . containerPullImageFlags ? . get ( imageName )
2019-09-12 10:52:05 +02:00
boolean pullImage = containerPullImage ! = null ? containerPullImage : config . dockerPullImage
2019-03-20 11:07:37 +02:00
def containerSpec = [
2019-09-12 10:52:05 +02:00
name : containerName . toLowerCase ( ) ,
image : imageName ,
imagePullPolicy: pullImage ? "Always" : "IfNotPresent" ,
env : getContainerEnvs ( config , imageName )
2018-10-04 17:06:42 +02:00
]
2019-03-20 11:07:37 +02:00
def configuredCommand = config . containerCommands ? . get ( imageName )
def shell = config . containerShell ? : '/bin/sh'
if ( configuredCommand = = null ) {
containerSpec [ 'command' ] = [
'/usr/bin/tail' ,
'-f' ,
'/dev/null'
]
2019-09-12 10:52:05 +02:00
} else if ( configuredCommand ! = "" ) {
2019-03-20 11:07:37 +02:00
// apparently "" is used as a flag for not settings container commands !?
containerSpec [ 'command' ] =
2019-09-12 10:52:05 +02:00
( configuredCommand in List ) ? configuredCommand : [
shell ,
'-c' ,
configuredCommand
]
2018-10-04 17:06:42 +02:00
}
if ( config . containerPortMappings ? . get ( imageName ) ) {
def ports = [ ]
def portCounter = 0
2019-09-12 10:52:05 +02:00
config . containerPortMappings . get ( imageName ) . each { mapping - >
2019-04-02 14:23:19 +02:00
def name = "${containerName}${portCounter}" . toString ( )
2019-09-12 10:52:05 +02:00
if ( mapping . containerPort ! = mapping . hostPort ) {
echo ( "[WARNING][${STEP_NAME}]: containerPort and hostPort are different for container '${containerName}'. "
2019-04-02 14:23:19 +02:00
+ "The hostPort will be ignored." )
}
ports . add ( [ name: name , containerPort: mapping . containerPort ] )
2019-09-12 10:52:05 +02:00
portCounter + +
2018-10-04 17:06:42 +02:00
}
2019-03-20 11:07:37 +02:00
containerSpec . ports = ports
2018-10-04 17:06:42 +02:00
}
2019-03-20 11:07:37 +02:00
result . push ( containerSpec )
2018-08-21 15:45:59 +02:00
}
2019-09-12 10:52:05 +02:00
if ( config . sidecarImage ) {
def containerSpec = [
name : config . sidecarName . toLowerCase ( ) ,
image : config . sidecarImage ,
imagePullPolicy: config . sidecarPullImage ? "Always" : "IfNotPresent" ,
env : getContainerEnvs ( config , config . sidecarImage ) ,
command : [ ]
]
result . push ( containerSpec )
}
2018-08-21 15:45:59 +02:00
return result
}
2019-02-08 13:20:45 +02:00
/ *
2018-08-21 15:45:59 +02:00
* Returns a list of envVar object consisting of set
* environment variables , params ( Parametrized Build ) and working directory .
* ( Kubernetes - Plugin only ! )
* @param config Map with configurations
* /
2019-09-12 10:52:05 +02:00
2018-10-04 17:06:42 +02:00
private List getContainerEnvs ( config , imageName ) {
2018-08-21 15:45:59 +02:00
def containerEnv = [ ]
2018-10-04 17:06:42 +02:00
def dockerEnvVars = config . containerEnvVars ? . get ( imageName ) ? : config . dockerEnvVars ? : [ : ]
def dockerWorkspace = config . containerWorkspaces ? . get ( imageName ) ! = null ? config . containerWorkspaces ? . get ( imageName ) : config . dockerWorkspace ? : ''
2018-08-21 15:45:59 +02:00
2019-03-20 11:07:37 +02:00
def envVar = { e - >
2019-09-12 10:52:05 +02:00
[ name: e . key , value: e . value ]
2019-03-20 11:07:37 +02:00
}
2018-08-21 15:45:59 +02:00
if ( dockerEnvVars ) {
for ( String k : dockerEnvVars . keySet ( ) ) {
containerEnv < < envVar ( key: k , value: dockerEnvVars [ k ] . toString ( ) )
}
}
if ( dockerWorkspace ) {
containerEnv < < envVar ( key: "HOME" , value: dockerWorkspace )
}
// Inherit the proxy information from the master to the container
SystemEnv systemEnv = new SystemEnv ( )
for ( String env : systemEnv . getEnv ( ) . keySet ( ) ) {
containerEnv < < envVar ( key: env , value: systemEnv . get ( env ) )
}
return containerEnv
}