-
Notifications
You must be signed in to change notification settings - Fork 593
/
dockerExecute.groovy
404 lines (369 loc) · 15.7 KB
/
dockerExecute.groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
import com.sap.piper.SidecarUtils
import static com.sap.piper.Prerequisites.checkScript
import com.cloudbees.groovy.cps.NonCPS
import com.sap.piper.ConfigurationHelper
import com.sap.piper.GenerateDocumentation
import com.sap.piper.JenkinsUtils
import com.sap.piper.Utils
import com.sap.piper.k8s.ContainerMap
import groovy.transform.Field
@Field def STEP_NAME = getClass().getName()
@Field def PLUGIN_ID_DOCKER_WORKFLOW = 'docker-workflow'
@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'
]
@Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS.plus([
/**
* Kubernetes only:
* Allows to specify start command for container created with dockerImage parameter to overwrite Piper default (`/usr/bin/tail -f /dev/null`).
*/
'containerCommand',
/**
* 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.
*/
'containerShell',
/**
* Kubernetes only: Allows to specify additional pod properties. For more details see step `dockerExecuteOnKubernetes`
*/
'additionalPodProperties',
/**
* Environment variables to set in the container, e.g. [http_proxy: 'proxy:8080'].
*/
'dockerEnvVars',
/**
* 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.
*/
'dockerImage',
/**
* The registry used for pulling the docker image, if left empty the default registry as defined by the `docker-commons-plugin` will be used.
*/
'dockerRegistryUrl',
/**
* Non Kubernetes only:
* The credentials for the docker registry of type username/password as we rely on docker jenkins plugin. If left empty, images are pulled anonymously.
* For Kubernetes cases, pass secret name of type `kubernetes.io/dockerconfigjson` via `additionalPodProperties` parameter (The secret should already be created and present in the environment)
*/
'dockerRegistryCredentialsId',
/**
* Same as `dockerRegistryUrl`, but for the sidecar. If left empty, `dockerRegistryUrl` is used instead.
*/
'sidecarRegistryUrl',
/**
* Same as `dockerRegistryCredentialsId`, but for the sidecar. If left empty `dockerRegistryCredentialsId` is used instead.
*/
'sidecarRegistryCredentialsId',
/**
* Kubernetes only:
* Name of the container launching `dockerImage`.
* SideCar only:
* Name of the container in local network.
*/
'dockerName',
/**
* Docker only:
* Docker options to be set when starting the container (List or String).
*/
'dockerOptions',
/**
* Docker only:
* Volumes that should be mounted into the container.
*/
'dockerVolumeBind',
/**
* 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
*/
'sidecarEnvVars',
/**
* as `dockerImage` for the sidecar container
*/
'sidecarImage',
/**
* as `dockerName` for the sidecar container
*/
'sidecarName',
/**
* as `dockerOptions` for the sidecar container
*/
'sidecarOptions',
/**
* as `dockerVolumeBind` for the sidecar container
*/
'sidecarVolumeBind',
/**
* as `dockerWorkspace` for the sidecar container
*/
'sidecarWorkspace',
/**
* Command executed inside the container which returns exit code 0 when the container is ready to be used.
*/
'sidecarReadyCommand',
/**
* Specific stashes that should be considered for the step execution.
*/
'stashContent'
])
@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',
])
@Field Map CONFIG_KEY_COMPATIBILITY = [
dockerRegistryCredentialsId: 'dockerRegistryCredentials',
sidecarRegistryCredentialsId: 'dockerSidecarRegistryCredentials',
]
/**
* 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
void call(Map parameters = [:], body) {
handlePipelineStepErrors(stepName: STEP_NAME, stepParameters: parameters, failOnError: true) {
final script = checkScript(this, parameters) ?: this
def utils = parameters.juStabUtils ?: new Utils()
String stageName = parameters.stageName ?: env.STAGE_NAME
Map config = ConfigurationHelper.newInstance(this)
.loadStepDefaults([:], stageName)
.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)
.use()
config = ConfigurationHelper.newInstance(this, config)
.addIfEmpty('sidecarRegistryUrl', config.dockerRegistryUrl)
.addIfEmpty('sidecarRegistryCredentialsId', config.dockerRegistryCredentialsId)
.use()
SidecarUtils sidecarUtils = new SidecarUtils(script)
utils.pushToSWA([
step: STEP_NAME,
stepParamKey1: 'scriptMissing',
stepParam1: parameters?.script == null,
stepParamKey2: 'kubernetes',
stepParam2: isKubernetes()
], config)
if (isKubernetes() && config.dockerImage) {
List dockerEnvVars = []
config.dockerEnvVars?.each { key, value ->
dockerEnvVars << "$key=$value"
}
if (env.POD_NAME && isContainerDefined(config)) {
container(getContainerDefined(config)) {
withEnv(dockerEnvVars) {
echo "[INFO][${STEP_NAME}] Executing inside a Kubernetes Container."
body()
sh "chown -R 1000:1000 ."
}
}
} else {
if (!config.dockerName) {
config.dockerName = UUID.randomUUID().toString()
}
def dockerExecuteOnKubernetesParams = [
script: script,
additionalPodProperties: config.additionalPodProperties,
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 += [
containerPortMappings: config.containerPortMappings,
sidecarName: parameters.sidecarName,
sidecarImage: parameters.sidecarImage,
sidecarPullImage: parameters.sidecarPullImage,
sidecarReadyCommand: parameters.sidecarReadyCommand,
sidecarEnvVars: parameters.sidecarEnvVars,
]
}
dockerExecuteOnKubernetes(dockerExecuteOnKubernetesParams) {
echo "[INFO][${STEP_NAME}] Executing inside a Kubernetes Pod"
body()
}
}
} 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
}
returnCode = sh script: 'docker ps -q > /dev/null', returnStatus: true
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
}
if (executeInsideDocker && config.dockerImage) {
utils.unstashAll(config.stashContent)
def image = docker.image(config.dockerImage)
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()
}
}
}
} finally {
sh "docker network remove ${networkName}"
}
}
}
} else {
echo "[INFO][${STEP_NAME}] Running on local environment."
body()
}
}
}
}
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
}
if (dockerCredentialsId) {
// 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) {
dockerImage.pull()
body()
}
} else {
dockerImage.pull()
body()
}
}
/*
* 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) {
def specialEnvironments = [
'http_proxy',
'https_proxy',
'no_proxy',
'HTTP_PROXY',
'HTTPS_PROXY',
'NO_PROXY'
]
def options = []
if (dockerEnvVars) {
dockerEnvVars.each { String k, v ->
options.add("--env ${k}=${v.toString()}")
}
}
specialEnvironments.each { String envVar ->
if (dockerEnvVars == null || !dockerEnvVars.containsKey(envVar)) {
options.add("--env ${envVar}")
}
}
if (dockerVolumeBind) {
dockerVolumeBind.each { String k, v ->
options.add("--volume ${k}:${v.toString()}")
}
}
if (dockerOptions) {
if (dockerOptions instanceof CharSequence) {
dockerOptions = [dockerOptions]
}
if (dockerOptions instanceof List) {
dockerOptions.each { String option ->
options << escapeBlanks(option)
}
} else {
throw new IllegalArgumentException("Unexpected type for dockerOptions. Expected was either a list or a string. Actual type was: '${dockerOptions.getClass()}'")
}
}
return options.join(' ')
}
boolean isContainerDefined(config) {
Map containerMap = ContainerMap.instance.getMap()
if (!containerMap.containsKey(env.POD_NAME)) {
return false
}
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
}
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)
}
/*
* 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
}