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', /** * The credentials for the docker registry. If left empty, images are pulled anonymously. */ '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.
* 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) new 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. description=Lorem ipsum is * changed to description=Lorem\ ipsum. */ @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 }