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  }