github.com/xgoffin/jenkins-library@v1.154.0/vars/dockerExecuteOnKubernetes.groovy (about) 1 import com.sap.piper.SidecarUtils 2 3 import static com.sap.piper.Prerequisites.checkScript 4 5 import com.sap.piper.ConfigurationHelper 6 import com.sap.piper.GenerateDocumentation 7 import com.sap.piper.JenkinsUtils 8 import com.sap.piper.Utils 9 import com.sap.piper.k8s.SystemEnv 10 import com.sap.piper.JsonUtils 11 12 import groovy.transform.Field 13 import hudson.AbortException 14 15 @Field def STEP_NAME = getClass().getName() 16 @Field def PLUGIN_ID_KUBERNETES = 'kubernetes' 17 18 @Field Set GENERAL_CONFIG_KEYS = [ 19 'jenkinsKubernetes', 20 /** 21 * Jnlp agent Docker images which should be used to create new pods. 22 * @parentConfigKey jenkinsKubernetes 23 */ 24 'jnlpAgent', 25 /** 26 * Namespace that should be used to create a new pod 27 * @parentConfigKey jenkinsKubernetes 28 */ 29 'namespace', 30 /** 31 * Name of the pod template that should be inherited from. 32 * The pod template can be defined in the Jenkins UI 33 * @parentConfigKey jenkinsKubernetes 34 */ 35 'inheritFrom', 36 'additionalPodProperties', 37 'resources', 38 'annotations', 39 /** 40 * Set this to 'false' to bypass a docker image pull. 41 * Useful during development process. Allows testing of images which are available in the local registry only. 42 */ 43 'dockerPullImage', 44 /** 45 * Set this to 'false' to bypass a docker image pull. 46 * Useful during development process. Allows testing of images which are available in the local registry only. 47 */ 48 'sidecarPullImage', 49 /** 50 * Print more detailed information into the log. 51 * @possibleValues `true`, `false` 52 */ 53 'verbose' 54 ] 55 @Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS.plus([ 56 57 /** 58 * Additional pod specific configuration. Map with the properties names 59 * as key and the corresponding value as value. The value can also be 60 * a nested structure. 61 * The properties will be added to the pod spec inside node `spec` at the 62 * same level like e.g. `containers`. 63 * This property provides some kind of an expert mode. Any property 64 * which is not handled otherwise by the step can be set. It is not 65 * possible to overwrite e.g. the `containers` property or to 66 * overwrite the `securityContext` property. 67 * Alternate way for providing `additionalPodProperties` is via 68 * `general/jenkinsKubernetes/additionalPodProperties` in the project configuration. 69 * Providing the resources map as parameter to the step call takes 70 * precedence. 71 * This freedom comes with great responsibility. The property 72 * `additionalPodProperties` should only be used in case you 73 * really know what you are doing. 74 */ 75 'additionalPodProperties', 76 /** 77 * Adds annotations in the metadata section of the PodSpec 78 */ 79 'annotations', 80 /** 81 * Allows to specify start command for container created with dockerImage parameter to overwrite Piper default (`/usr/bin/tail -f /dev/null`). 82 */ 83 'containerCommand', 84 /** 85 * Specifies start command for containers to overwrite Piper default (`/usr/bin/tail -f /dev/null`). 86 * If container's defaultstart command should be used provide empty string like: `['selenium/standalone-chrome': '']`. 87 */ 88 'containerCommands', 89 /** 90 * Specifies environment variables per container. If not provided `dockerEnvVars` will be used. 91 */ 92 'containerEnvVars', 93 /** 94 * 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 labelled based on the value field of each map entry. 95 * Example: `['maven:3.5-jdk-8-alpine': 'mavenExecute', 'selenium/standalone-chrome': 'selenium', 'famiko/jmeter-base': 'checkJMeter', 'ppiper/cf-cli:6': 'cloudfoundry']` 96 */ 97 'containerMap', 98 /** 99 * Optional configuration in combination with containerMap to define the container where the commands should be executed in. 100 */ 101 'containerName', 102 /** 103 * Map which defines per docker image the port mappings, e.g. `containerPortMappings: ['selenium/standalone-chrome': [[name: 'selPort', containerPort: 4444, hostPort: 4444]]]`. 104 */ 105 'containerPortMappings', 106 /** 107 * Specifies the pullImage flag per container. 108 */ 109 'containerPullImageFlags', 110 /** 111 * Allows to specify the shell to be executed for container with containerName. 112 */ 113 'containerShell', 114 /** 115 * 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. 116 */ 117 'containerWorkspaces', 118 /** 119 * Environment variables to set in the container, e.g. [http_proxy:'proxy:8080']. 120 */ 121 'dockerEnvVars', 122 /** 123 * Optional name of the docker image that should be used. If no docker image is provided, the closure will be executed in the jnlp agent container. 124 */ 125 'dockerImage', 126 /** 127 * Specifies a dedicated user home directory for the container which will be passed as value for environment variable `HOME`. 128 */ 129 'dockerWorkspace', 130 /** 131 * as `dockerImage` for the sidecar container 132 */ 133 'sidecarImage', 134 /** 135 * SideCar only: 136 * Name of the container in local network. 137 */ 138 'sidecarName', 139 /** 140 * Command executed inside the container which returns exit code 0 when the container is ready to be used. 141 */ 142 'sidecarReadyCommand', 143 /** 144 * as `dockerEnvVars` for the sidecar container 145 */ 146 'sidecarEnvVars', 147 /** 148 * as `dockerWorkspace` for the sidecar container 149 */ 150 'sidecarWorkspace', 151 152 /** Defines the Kubernetes nodeSelector as per [https://github.com/jenkinsci/kubernetes-plugin](https://github.com/jenkinsci/kubernetes-plugin).*/ 153 'nodeSelector', 154 /** 155 * Kubernetes Security Context used for the pod. 156 * Can be used to specify uid and fsGroup. 157 * See: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ 158 */ 159 'securityContext', 160 /** 161 * Specific stashes that should be considered for the step execution. 162 */ 163 'stashContent', 164 /** 165 * In the Kubernetes case the workspace is only available to the respective Jenkins slave but not to the containers running inside the pod.<br /> 166 * This configuration defines exclude pattern for stashing from Jenkins workspace to working directory in container and back. 167 * Following excludes can be set: 168 * 169 * * `workspace`: Pattern for stashing towards container 170 * * `stashBack`: Pattern for bringing data from container back to Jenkins workspace. If not set: defaults to setting for `workspace`. 171 */ 172 'stashExcludes', 173 /** 174 * In the Kubernetes case the workspace is only available to the respective Jenkins slave but not to the containers running inside the pod.<br /> 175 * This configuration defines include pattern for stashing from Jenkins workspace to working directory in container and back. 176 * Following includes can be set: 177 * 178 * * `workspace`: Pattern for stashing towards container 179 * * `stashBack`: Pattern for bringing data from container back to Jenkins workspace. If not set: defaults to setting for `workspace`. 180 */ 181 'stashIncludes', 182 /** 183 * In the Kubernetes case the workspace is only available to the respective Jenkins slave but not to the containers running inside the pod.<br /> 184 * This configuration defines include pattern for stashing from Jenkins workspace to working directory in container and back. 185 * This flag controls whether the stashing does *not* use the default exclude patterns in addition to the patterns provided in `stashExcludes`. 186 * @possibleValues `true`, `false` 187 */ 188 'stashNoDefaultExcludes', 189 /** 190 * A map containing the resources per container. The key is the 191 * container name. The value is a map defining valid resources. 192 * An entry with key `DEFAULT` can be used for defining resources 193 * for all containers which does not have resources specified otherwise. 194 * Alternate way for providing resources is via `general/jenkinsKubernetes/resources` 195 * in the project configuration. Providing the resources map as parameter 196 * to the step call takes precedence. 197 */ 198 'resources', 199 /** 200 * The path to which a volume should be mounted to. This volume will be available at the same 201 * mount path in each container of the provided containerMap. The volume is of type emptyDir 202 * and has the name 'volume'. With the additionalPodProperties parameter one can for example 203 * use this volume in an initContainer. 204 */ 205 'containerMountPath', 206 /** 207 * The docker image to run as initContainer. 208 */ 209 'initContainerImage', 210 /** 211 * Command executed inside the init container shell. Please enter command without providing any "sh -c" prefix. For example for an echo message, simply enter: echo `HelloWorld` 212 */ 213 'initContainerCommand', 214 215 ]) 216 @Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS.minus([ 217 'stashIncludes', 218 'stashExcludes' 219 ]) 220 221 /** 222 * Executes a closure inside a container in a kubernetes pod. 223 * Proxy environment variables defined on the Jenkins machine are also available in the container. 224 * 225 * By default jnlp agent defined for kubernetes-plugin will be used (see [https://github.com/jenkinsci/kubernetes-plugin#pipeline-support](https://github.com/jenkinsci/kubernetes-plugin#pipeline-support)). 226 * 227 * It is possible to define a custom jnlp agent image by 228 * 229 * 1. Defining the jnlp image via environment variable JENKINS_JNLP_IMAGE in the Kubernetes landscape 230 * 2. Defining the image via config (`jenkinsKubernetes.jnlpAgent`) 231 * 232 * Option 1 will take precedence over option 2. 233 */ 234 @GenerateDocumentation 235 void call(Map parameters = [:], body) { 236 handlePipelineStepErrors(stepName: STEP_NAME, stepParameters: parameters, failOnError: true) { 237 238 final script = checkScript(this, parameters) ?: this 239 def utils = parameters.juStabUtils ?: new Utils() 240 String stageName = parameters.stageName ?: env.STAGE_NAME 241 242 if (!JenkinsUtils.isPluginActive(PLUGIN_ID_KUBERNETES)) { 243 error("[ERROR][${STEP_NAME}] not supported. Plugin '${PLUGIN_ID_KUBERNETES}' is not installed or not active.") 244 } 245 246 Map config = ConfigurationHelper.newInstance(this) 247 .loadStepDefaults([:], stageName) 248 .mixinGeneralConfig(script.commonPipelineEnvironment, GENERAL_CONFIG_KEYS) 249 .mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS) 250 .mixinStageConfig(script.commonPipelineEnvironment, stageName, STEP_CONFIG_KEYS) 251 .mixin(parameters, PARAMETER_KEYS) 252 .addIfEmpty('uniqueId', UUID.randomUUID().toString()) 253 .use() 254 255 utils.pushToSWA([ 256 step : STEP_NAME, 257 stepParamKey1: 'scriptMissing', 258 stepParam1 : parameters?.script == null 259 ], config) 260 261 if (!config.containerMap && config.dockerImage) { 262 config.containerName = 'container-exec' 263 config.containerMap = [(config.get('dockerImage')): config.containerName] 264 config.containerCommands = config.containerCommand ? [(config.get('dockerImage')): config.containerCommand] : null 265 } 266 executeOnPod(config, utils, body, script) 267 } 268 } 269 270 def getOptions(config) { 271 def namespace = config.jenkinsKubernetes.namespace 272 def options = [ 273 name : 'dynamic-agent-' + config.uniqueId, 274 label: config.uniqueId, 275 yaml : generatePodSpec(config) 276 ] 277 if (namespace) { 278 options.namespace = namespace 279 } 280 if (config.nodeSelector) { 281 options.nodeSelector = config.nodeSelector 282 } 283 if (!config.verbose) { 284 options.showRawYaml = false 285 } 286 287 if(config.jenkinsKubernetes.inheritFrom){ 288 options.inheritFrom = config.jenkinsKubernetes.inheritFrom 289 options.yamlMergeStrategy = merge() 290 } 291 return options 292 } 293 294 void executeOnPod(Map config, utils, Closure body, Script script) { 295 /* 296 * There could be exceptions thrown by 297 - The podTemplate 298 - The container method 299 - The body 300 * We use nested exception handling in this case. 301 * In the first 2 cases, the 'container' stash is not created because the inner try/finally is not reached. 302 * However, the workspace has not been modified and don't need to be restored. 303 * In case third case, we need to create the 'container' stash to bring the modified content back to the host. 304 */ 305 try { 306 SidecarUtils sidecarUtils = new SidecarUtils(script) 307 def stashContent = config.stashContent 308 if (config.containerName && stashContent.isEmpty()) { 309 stashContent = [stashWorkspace(config, 'workspace')] 310 } 311 podTemplate(getOptions(config)) { 312 node(config.uniqueId) { 313 if (config.sidecarReadyCommand) { 314 sidecarUtils.waitForSidecarReadyOnKubernetes(config.sidecarName, config.sidecarReadyCommand) 315 } 316 if (config.containerName) { 317 Map containerParams = [name: config.containerName] 318 if (config.containerShell) { 319 containerParams.shell = config.containerShell 320 } 321 echo "ContainerConfig: ${containerParams}" 322 container(containerParams) { 323 try { 324 utils.unstashAll(stashContent) 325 echo "invalidate stash workspace-${config.uniqueId}" 326 stash name: "workspace-${config.uniqueId}", excludes: '**/*', allowEmpty: true 327 body() 328 } finally { 329 stashWorkspace(config, 'container', true, true) 330 } 331 } 332 } else { 333 body() 334 } 335 } 336 } 337 } finally { 338 if (config.containerName) 339 unstashWorkspace(config, 'container') 340 } 341 } 342 343 private String generatePodSpec(Map config) { 344 def podSpec = [ 345 apiVersion: "v1", 346 kind : "Pod", 347 metadata : [ 348 lables: config.uniqueId, 349 annotations: [:] 350 ], 351 spec : [:] 352 ] 353 podSpec.metadata.annotations = getAnnotations(config) 354 podSpec.spec += getAdditionalPodProperties(config) 355 podSpec.spec.initContainers = getInitContainerList(config) 356 podSpec.spec.containers = getContainerList(config) 357 podSpec.spec.securityContext = getSecurityContext(config) 358 359 if (config.containerMountPath) { 360 podSpec.spec.volumes = [[ 361 name : "volume", 362 emptyDir: [:] 363 ]] 364 } 365 366 return new JsonUtils().groovyObjectToPrettyJsonString(podSpec) 367 } 368 369 private String stashWorkspace(config, prefix, boolean chown = false, boolean stashBack = false) { 370 def stashName = "${prefix}-${config.uniqueId}" 371 try { 372 if (chown) { 373 def securityContext = getSecurityContext(config) 374 def runAsUser = securityContext?.runAsUser ?: 1000 375 def fsGroup = securityContext?.fsGroup ?: 1000 376 sh """#!${config.containerShell ?: '/bin/sh'} 377 chown -R ${runAsUser}:${fsGroup} .""" 378 } 379 380 def includes, excludes 381 382 if (stashBack) { 383 includes = config.stashIncludes.stashBack ?: config.stashIncludes.workspace 384 excludes = config.stashExcludes.stashBack ?: config.stashExcludes.workspace 385 } else { 386 includes = config.stashIncludes.workspace 387 excludes = config.stashExcludes.workspace 388 } 389 390 stash( 391 name: stashName, 392 includes: includes, 393 excludes: excludes, 394 // 'true' by default due to negative side-effects, but can be overwritten via parameters 395 // (as done by artifactPrepareVersion to preserve the .git folder) 396 useDefaultExcludes: !config.stashNoDefaultExcludes, 397 ) 398 return stashName 399 } catch (AbortException | IOException e) { 400 echo "${e.getMessage()}" 401 } catch (Throwable e) { 402 echo "Unstash workspace failed with throwable ${e.getMessage()}" 403 throw e 404 } 405 return null 406 } 407 408 private Map getAnnotations(Map config){ 409 return config.annotations ?: config.jenkinsKubernetes.annotations ?: [:] 410 } 411 412 private Map getAdditionalPodProperties(Map config) { 413 Map podProperties = config.additionalPodProperties ?: config.jenkinsKubernetes.additionalPodProperties ?: [:] 414 if(podProperties) { 415 echo "Additional pod properties found (${podProperties.keySet()})." + 416 ' Providing additional pod properties is some kind of expert mode. In case of any problems caused by these' + 417 ' additional properties only limited support can be provided.' 418 } 419 return podProperties 420 } 421 422 private Map getSecurityContext(Map config) { 423 return config.securityContext ?: config.jenkinsKubernetes.securityContext ?: [:] 424 } 425 426 private void unstashWorkspace(config, prefix) { 427 try { 428 unstash "${prefix}-${config.uniqueId}" 429 } catch (AbortException | IOException e) { 430 echo "${e.getMessage()}\n${e.getCause()}" 431 } catch (Throwable e) { 432 echo "Unstash workspace failed with throwable ${e.getMessage()}" 433 throw e 434 } finally { 435 echo "invalidate stash ${prefix}-${config.uniqueId}" 436 stash name: "${prefix}-${config.uniqueId}", excludes: '**/*', allowEmpty: true 437 } 438 } 439 440 private List getInitContainerList(config){ 441 def initContainerSpecList = [] 442 if (config.initContainerImage && config.containerMountPath) { 443 // regex [\W_] matches any non-word character equivalent to [^a-zA-Z0-9_] 444 def initContainerName = config.initContainerImage.toLowerCase().replaceAll(/[\W_]/,"-" ) 445 def initContainerSpec = [ 446 name : initContainerName, 447 image : config.initContainerImage 448 ] 449 if (config.containerMountPath) { 450 initContainerSpec.volumeMounts = [[name: "volume", mountPath: config.containerMountPath]] 451 } 452 if (config.initContainerCommand == null) { 453 initContainerSpec['command'] = [ 454 '/usr/bin/tail', 455 '-f', 456 '/dev/null' 457 ] 458 } else { 459 initContainerSpec['command'] = [ 460 'sh', 461 '-c', 462 config.initContainerCommand 463 ] 464 } 465 initContainerSpecList.push(initContainerSpec) 466 } 467 return initContainerSpecList 468 } 469 470 private List getContainerList(config) { 471 472 //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 473 def result = [] 474 //allow definition of jnlp image via environment variable JENKINS_JNLP_IMAGE in the Kubernetes landscape or via config as fallback 475 if (env.JENKINS_JNLP_IMAGE || config.jenkinsKubernetes.jnlpAgent) { 476 477 def jnlpContainerName = 'jnlp' 478 479 def jnlpSpec = [ 480 name : jnlpContainerName, 481 image: env.JENKINS_JNLP_IMAGE ?: config.jenkinsKubernetes.jnlpAgent 482 ] 483 484 def resources = getResources(jnlpContainerName, config) 485 if(resources) { 486 jnlpSpec.resources = resources 487 } 488 489 result.push(jnlpSpec) 490 } 491 config.containerMap.each { imageName, containerName -> 492 def containerPullImage = config.containerPullImageFlags?.get(imageName) 493 boolean pullImage = containerPullImage != null ? containerPullImage : config.dockerPullImage 494 def containerSpec = [ 495 name : containerName.toLowerCase(), 496 image : imageName, 497 imagePullPolicy: pullImage ? "Always" : "IfNotPresent", 498 env : getContainerEnvs(config, imageName, config.dockerEnvVars, config.dockerWorkspace) 499 ] 500 if (config.containerMountPath) { 501 containerSpec.volumeMounts = [[name: "volume", mountPath: config.containerMountPath]] 502 } 503 504 def configuredCommand = config.containerCommands?.get(imageName) 505 def shell = config.containerShell ?: '/bin/sh' 506 if (configuredCommand == null) { 507 containerSpec['command'] = [ 508 '/usr/bin/tail', 509 '-f', 510 '/dev/null' 511 ] 512 } else if (configuredCommand != "") { 513 // apparently "" is used as a flag for not settings container commands !? 514 containerSpec['command'] = 515 (configuredCommand in List) ? configuredCommand : [ 516 shell, 517 '-c', 518 configuredCommand 519 ] 520 } 521 522 if (config.containerPortMappings?.get(imageName)) { 523 def ports = [] 524 def portCounter = 0 525 config.containerPortMappings.get(imageName).each { mapping -> 526 def name = "${containerName}${portCounter}".toString() 527 if (mapping.containerPort != mapping.hostPort) { 528 echo("[WARNING][${STEP_NAME}]: containerPort and hostPort are different for container '${containerName}'. " 529 + "The hostPort will be ignored.") 530 } 531 ports.add([name: name, containerPort: mapping.containerPort]) 532 portCounter++ 533 } 534 containerSpec.ports = ports 535 } 536 def resources = getResources(containerName.toLowerCase(), config) 537 if(resources) { 538 containerSpec.resources = resources 539 } 540 result.push(containerSpec) 541 } 542 if (config.sidecarImage) { 543 def sideCarContainerName = config.sidecarName.toLowerCase() 544 def containerSpec = [ 545 name : sideCarContainerName, 546 image : config.sidecarImage, 547 imagePullPolicy: config.sidecarPullImage ? "Always" : "IfNotPresent", 548 env : getContainerEnvs(config, config.sidecarImage, config.sidecarEnvVars, config.sidecarWorkspace), 549 command : [] 550 ] 551 def resources = getResources(sideCarContainerName, config) 552 if(resources) { 553 containerSpec.resources = resources 554 } 555 result.push(containerSpec) 556 } 557 return result 558 } 559 560 private Map getResources(String containerName, Map config) { 561 Map resources = config.resources 562 if(resources == null) { 563 resources = config?.jenkinsKubernetes.resources 564 } 565 if(resources == null) { 566 return null 567 } 568 Map res = resources.get(containerName) 569 if(res == null) { 570 res = resources.get('DEFAULT') 571 } 572 return res 573 } 574 575 /* 576 * Returns a list of envVar object consisting of set 577 * environment variables, params (Parametrized Build) and working directory. 578 * (Kubernetes-Plugin only!) 579 * @param config Map with configurations 580 */ 581 582 private List getContainerEnvs(config, imageName, defaultEnvVars, defaultConfig) { 583 def containerEnv = [] 584 def dockerEnvVars = config.containerEnvVars?.get(imageName) ?: defaultEnvVars ?: [:] 585 def dockerWorkspace = config.containerWorkspaces?.get(imageName) != null ? config.containerWorkspaces?.get(imageName) : defaultConfig ?: '' 586 587 def envVar = { e -> 588 [name: e.key, value: e.value] 589 } 590 591 if (dockerEnvVars) { 592 dockerEnvVars.each { 593 k, v -> 594 containerEnv << envVar(key: k, value: v.toString()) 595 } 596 } 597 598 if (dockerWorkspace) { 599 containerEnv << envVar(key: "HOME", value: dockerWorkspace) 600 } 601 602 // Inherit the proxy information from the master to the container 603 SystemEnv systemEnv = new SystemEnv() 604 systemEnv.getEnv().each { 605 k, v -> 606 containerEnv << envVar(key: k, value: v) 607 } 608 609 return containerEnv 610 }