Jenkins Base Install

Using the public Docker image as a baseline https://github.com/jenkinsci/docker, the majority of the Dockerfile has been adjusted to support our initial base installation with the following components:

  • JDK11 Image
  • No Install wizard
  • Preinstall Plugins for workflow
  • Configure Credentials for SCM and Nexus3
labimagesjenkins
version: '3.3'

services:
  jenkins:
    image: infra/jenkins:1.0.0
    build:
      context: .
      network: host
      args:
        ARG_ART_URL: http://d1i-doc-ngbuild:3001
      extra_hosts:
        - "d1i-doc-ngbuild:172.22.90.2"
FROM infra/java/openjdk-11:1.0.0

ARG ARG_ART_URL

RUN sed -e "s|APT_URL|${ARG_ART_URL}|" /etc/apt/sources.list.base > /etc/apt/sources.list \
    && apt-get update \
    && apt-get install -y curl git unzip dos2unix \
    && apt-get clean \
    && rm /etc/apt/sources.list

ENV JENKINS_HOME /var/jenkins_home

ARG user=jenkins
ARG group=${user}
ARG uid=1000
ARG gid=1000

RUN groupadd -g ${gid} ${group} \
    && useradd -d "$JENKINS_HOME" -u ${uid} -g ${gid} -m -s /bin/bash ${user}


ARG ARG_ART_URL

###############################################################
## Install Jenkins & Plugin Manager
###############################################################

RUN mkdir -p /usr/share/jenkins \
    && curl $ARG_ART_URL/repository/dml/docker/jenkins/jenkins-war-2.346.3.war -o /usr/share/jenkins/jenkins.war \
    && curl $ARG_ART_URL/repository/dml/docker/jenkins/jenkins-plugin-manager-2.1.0.jar -o /usr/lib/jenkins-plugin-manager.jar

COPY jenkins-plugin-cli.sh /usr/local/bin/jenkins-plugin-cli.sh
RUN chmod +x /usr/local/bin/jenkins-plugin-cli.sh \
    && dos2unix /usr/local/bin/jenkins-plugin-cli.sh

###############################################################
## Core Structure and permissions
###############################################################

# `/usr/share/jenkins/ref/` for bundles, needed in jenkins.sh check
RUN mkdir -p /usr/share/jenkins/ref/init.groovy.d

RUN chown -R ${user} "$JENKINS_HOME" /usr/share/jenkins/ref

# startup scripts
COPY jenkins-support /usr/local/bin/jenkins-support
COPY jenkins.sh /usr/local/bin/jenkins.sh

RUN chmod +x /usr/local/bin/jenkins-support \
    && chmod +x /usr/local/bin/jenkins.sh \
    && dos2unix /usr/local/bin/jenkins-support \
	&& dos2unix /usr/local/bin/jenkins.sh

# needed for jenkins.sh and jenkins-support
# specifically for volume persmission check
ENV COPY_REFERENCE_FILE_LOG $JENKINS_HOME/copy_reference_file.log

# for jenkins options logs
#RUN mkdir /var/log/jenkins
#RUN chown -R ${user}:${user} /var/log/jenkins

# for jenkins options cache
#RUN mkdir /var/cache/jenkins
#RUN chown -R ${user}:${user} /var/cache/jenkins

# needed for jenkins.sh execution
ENV JAVA_OPTS="-Xmx2048m -Djenkins.install.runSetupWizard=false"
#ENV JENKINS_OPTS="--logfile=/var/log/jenkins/jenkins.log --webroot=/var/cache/jenkins/war"

###############################################################
# get tini for docker processes
###############################################################

RUN curl $ARG_ART_URL/repository/dml/docker/tools/tini -o /bin/tini \
    && chmod +x /bin/tini

###############################################################
## Actual Plugins for this image, this will change frequently
## for each possible build base 
## so putting it here at the end for image optimization
###############################################################

# plugins for this image
COPY plugins-01.txt /tmp/plugins-01.txt
RUN /usr/local/bin/jenkins-plugin-cli.sh -f /tmp/plugins-01.txt

# init files via groovy
COPY init-01.groovy /usr/share/jenkins/ref/init.groovy.d/init-01.groovy
RUN chmod +x /usr/share/jenkins/ref/init.groovy.d/init-01.groovy

###############################################################
## Final Steps
###############################################################

# Switch to the jenkins user
USER ${user}

# only needed if you connect to a repo with a non-public cert
RUN git config --global http.sslVerify "false"

ENTRYPOINT ["/bin/tini", "--", "/usr/local/bin/jenkins.sh"]
import hudson.model.*;
import jenkins.model.*;

import hudson.tasks.Maven.MavenInstallation;
import hudson.tools.InstallSourceProperty;
import hudson.tools.ToolProperty;
import hudson.tools.ToolPropertyDescriptor;
import hudson.util.DescribableList;

import com.cloudbees.plugins.credentials.impl.*;
import com.cloudbees.plugins.credentials.*;
import com.cloudbees.plugins.credentials.domains.*;
import com.cloudbees.jenkins.plugins.sshcredentials.impl.*

Thread.start {
      
      println "--> starting init-01"

      /*
       * Step 1 - check if we are already configured based on credentials existing
       */

      List<Credentials> existing = SystemCredentialsProvider.getInstance().getCredentials();
      if (existing.size() > 0) {
            println "Jenkins already configured, skipping."
            return;
      }

      /*
       * Step 2 - we want to use 3 sets of credentiuals, one for artifacts
       *          one for scm if needed, another for ssn if needed
       */

       def env = System.getenv()

      //https://github.com/jenkinsci/jenkins-scripts/blob/master/scriptler/addCredentials.groovy
      
      def artifactsPassword = new File('/run/secrets/jenkins-passwords/artifacts-pwd.txt').text
      Credentials artifactCredentials = (Credentials) new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, env['artifacts.id'], env['artifacts.id'], env['artifacts.user'], artifactsPassword)
      SystemCredentialsProvider.getInstance().getStore().addCredentials(Domain.global(), artifactCredentials)
      
      println "--> added artifacts credentials from env and secrets"

      if (env['scm.id'] != null) {

            def scmPassword = new File('/run/secrets/jenkins-passwords/scm-pwd.txt').text
            Credentials scmCredentials = (Credentials) new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, env['scm.id'], env['scm.id'], env['scm.user'], scmPassword)
            SystemCredentialsProvider.getInstance().getStore().addCredentials(Domain.global(), scmCredentials)

            println "--> added scm credentials from env and secrets"
      }

      if (env['ssh.id'] != null) {

            // https://github.com/jenkinsci/ssh-credentials-plugin/blob/master/src/main/java/com/cloudbees/jenkins/plugins/sshcredentials/impl/BasicSSHUserPrivateKey.java

            def sshPassphrase = new File('/run/secrets/jenkins-passwords/ssh-passphrase.txt').text

            // for proper env independance we need to worry about dos2unix for win to linux file issues
            def sshPrivateKey = new File('/run/secrets/jenkins-passwords/ssh-private-key.txt').text.replaceAll("\r\n", "\n")

            def source = new BasicSSHUserPrivateKey.DirectEntryPrivateKeySource(sshPrivateKey)
            def ssh = new BasicSSHUserPrivateKey(CredentialsScope.GLOBAL, env['ssh.id'], env['ssh.id'], source, sshPassphrase, env['ssh.id'])
            SystemCredentialsProvider.getInstance().getStore().addCredentials(Domain.global(), ssh)

            println "--> added ssh credentials from env and secrets"
      }
      
}     
#!/usr/bin/env bash

java -jar /usr/lib/jenkins-plugin-manager.jar "$@"
#!/bin/bash -eu

# compare if version1 < version2
versionLT() {
    local v1; v1=$(echo "$1" | cut -d '-' -f 1 )
    local q1; q1=$(echo "$1" | cut -s -d '-' -f 2- )
    local v2; v2=$(echo "$2" | cut -d '-' -f 1 )
    local q2; q2=$(echo "$2" | cut -s -d '-' -f 2- )
    if [ "$v1" = "$v2" ]; then
        if [ "$q1" = "$q2" ]; then
            return 1
        else
            if [ -z "$q1" ]; then
                return 1
            else
                if [ -z "$q2" ]; then
                    return 0
                else
                    [  "$q1" = "$(echo -e "$q1\n$q2" | sort -V | head -n1)" ]
                fi
            fi
        fi
    else
        [  "$v1" = "$(echo -e "$v1\n$v2" | sort -V | head -n1)" ]
    fi
}

# returns a plugin version from a plugin archive
get_plugin_version() {
    local archive; archive=$1
    local version; version=$(unzip -p "$archive" META-INF/MANIFEST.MF | grep "^Plugin-Version: " | sed -e 's#^Plugin-Version: ##')
    version=${version%%[[:space:]]}
    echo "$version"
}

# Copy files from /usr/share/jenkins/ref into $JENKINS_HOME
# So the initial JENKINS-HOME is set with expected content.
# Don't override, as this is just a reference setup, and use from UI
# can then change this, upgrade plugins, etc.
copy_reference_file() {
    f="${1%/}"
    b="${f%.override}"
    rel="${b:23}"
    version_marker="${rel}.version_from_image"
    dir=$(dirname "${b}")
    local action;
    local reason;
    local container_version;
    local image_version;
    local marker_version;
    local log; log=false
    if [[ ${rel} == plugins/*.jpi ]]; then
        container_version=$(get_plugin_version "$JENKINS_HOME/${rel}")
        image_version=$(get_plugin_version "${f}")
        if [[ -e $JENKINS_HOME/${version_marker} ]]; then
            marker_version=$(cat "$JENKINS_HOME/${version_marker}")
            if versionLT "$marker_version" "$container_version"; then
                action="SKIPPED"
                reason="Installed version ($container_version) has been manually upgraded from initial version ($marker_version)"
                log=true
            else
                if [[ "$image_version" == "$container_version" ]]; then
                    action="SKIPPED"
                    reason="Version from image is the same as the installed version $image_version"
                else
                    if versionLT "$image_version" "$container_version"; then
                        action="SKIPPED"
                        log=true
                        reason="Image version ($image_version) is older than installed version ($container_version)"
                    else
                        action="UPGRADED"
                        log=true
                        reason="Image version ($image_version) is newer than installed version ($container_version)"
                    fi
                fi
            fi
        else
            if [[ -n "$TRY_UPGRADE_IF_NO_MARKER" ]]; then
                if [[ "$image_version" == "$container_version" ]]; then
                    action="SKIPPED"
                    reason="Version from image is the same as the installed version $image_version (no marker found)"
                    # Add marker for next time
                    echo "$image_version" > "$JENKINS_HOME/${version_marker}"
                else
                    if versionLT "$image_version" "$container_version"; then
                        action="SKIPPED"
                        log=true
                        reason="Image version ($image_version) is older than installed version ($container_version) (no marker found)"
                    else
                        action="UPGRADED"
                        log=true
                        reason="Image version ($image_version) is newer than installed version ($container_version) (no marker found)"
                    fi
                fi
            fi
        fi
        if [[ ! -e $JENKINS_HOME/${rel} || "$action" == "UPGRADED" || $f = *.override ]]; then
            action=${action:-"INSTALLED"}
            log=true
            mkdir -p "$JENKINS_HOME/${dir:23}"
            cp -pr "${f}" "$JENKINS_HOME/${rel}";
            # pin plugins on initial copy
            touch "$JENKINS_HOME/${rel}.pinned"
            echo "$image_version" > "$JENKINS_HOME/${version_marker}"
            reason=${reason:-$image_version}
        else
            action=${action:-"SKIPPED"}
        fi
    else
        if [[ ! -e $JENKINS_HOME/${rel} || $f = *.override ]]
        then
            action="INSTALLED"
            log=true
            mkdir -p "$JENKINS_HOME/${dir:23}"
            cp -pr "${f}" "$JENKINS_HOME/${rel}";
        else
            action="SKIPPED"
        fi
    fi
    if [[ -n "$VERBOSE" || "$log" == "true" ]]; then
        if [ -z "$reason" ]; then
            echo "$action $rel" >> "$COPY_REFERENCE_FILE_LOG"
        else
            echo "$action $rel : $reason" >> "$COPY_REFERENCE_FILE_LOG"
        fi
    fi
}
#! /bin/bash -e

: "${JENKINS_WAR:="/usr/share/jenkins/jenkins.war"}"
: "${JENKINS_HOME:="/var/jenkins_home"}"
touch "${COPY_REFERENCE_FILE_LOG}" || { echo "Can not write to ${COPY_REFERENCE_FILE_LOG}. Wrong volume permissions?"; exit 1; }
echo "--- Copying files at $(date)" >> "$COPY_REFERENCE_FILE_LOG"
find /usr/share/jenkins/ref/ \( -type f -o -type l \) -exec bash -c '. /usr/local/bin/jenkins-support; for arg; do copy_reference_file "$arg"; done' _ {} +

# if `docker run` first argument start with `--` the user is passing jenkins launcher arguments
if [[ $# -lt 1 ]] || [[ "$1" == "--"* ]]; then

  # read JAVA_OPTS and JENKINS_OPTS into arrays to avoid need for eval (and associated vulnerabilities)
  java_opts_array=()
  while IFS= read -r -d '' item; do
    java_opts_array+=( "$item" )
  done < <([[ $JAVA_OPTS ]] && xargs printf '%s\0' <<<"$JAVA_OPTS")

  jenkins_opts_array=( )
  while IFS= read -r -d '' item; do
    jenkins_opts_array+=( "$item" )
  done < <([[ $JENKINS_OPTS ]] && xargs printf '%s\0' <<<"$JENKINS_OPTS")

  exec java -Duser.home="$JENKINS_HOME" "${java_opts_array[@]}" -jar ${JENKINS_WAR} "${jenkins_opts_array[@]}" "$@"
fi

# As argument is not jenkins, assume user want to run his own process, for example a `bash` shell to explore this image
exec "$@"
workflow-aggregator:2.6
git:4.8.2
pipeline-utility-steps:2.10.0
http_request:1.11

As we are disabling the installation wizard via -Djenkins.install.runSetupWizard=false, we will be using the jenkins-plugin-manager.jar in combination with the plugins.txt to install plugins (and their dependencies). For our installation the following plugins will be installed:

workflow-aggregator:2.6
git:4.8.2
pipeline-utility-steps:2.10.0
http_request:1.11

A new plugin Manager is available at https://github.com/jenkinsci/plugin-installation-manager-tool with the intend of replacing the above plugin process. In the future we may update our install to use the new CLI.

The majority of the installation and configuration is done via our init-01.groovy script responsible for taking our injected secrets and creating the appropriate Jenkins credentials for use in builds.

A typical docker compose provides the environment based variables that will be needed to faciliate the automated install and config.

services:
    jenkins:
    [...]
    environment:
        - artifacts.id=nexus
        - artifacts.user=admin
        - scm.id=bitbucket
        - scm.user=admin
        - ssh.id=github
    secrets:
        - jenkins-passwords
secrets:
jenkins-passwords:
    #artifacts-pwd.txt
    #scm-pwd.txt
    #ssh-passphrase.txt
    #ssh-private-key.txt
    file: secrets/jenkins-passwords

The following secrets are generally needed when dealing with jenkins pipelines:

  • (required) Credentials for Artifact Storage, in our case Nexus 3
  • (optional) Credentials for User/Password based Source Control
  • (optional) Credentials for SSH keys to support for example private Github repos

Building and Running the Base Image

Build the Base image.

docker-compose -f images/jenkins/docker-compose.yml build

#docker login docker-private.acme.com
images/docker-push.sh infra/jenkins:1.0.0 docker-private.acme.com

Running is done via a compose that includes secrets and optional configs for volumes.

labcomposed
admin123
admin123
version: '3.3'

services:
  jenkins:
    image: infra/jenkins:1.0.0
    ports:
      - "8080:8080"
    #volumes:
    #  - jenkins-logs:/var/log/jenkins
    #  - jenkins-data:/var/jenkins_home
    environment:
      - artifacts.id=nexus
      - artifacts.user=admin
      - scm.id=bitbucket
      - scm.user=admin
      - ssh.id=github
    secrets:
      - jenkins-passwords
    networks:
      - ops-network
    extra_hosts:
      - "bitbucket.acme.com:172.22.90.1"
      - "nexus.acme.com:172.22.90.1"
networks:
  ops-network:
    name: ops-network
volumes:
  jenkins-data:
    name: jenkins-data
  jenkins-logs:
    name: jenkins-logs
secrets:
  jenkins-passwords:
    #artifacts-pwd.txt
    #scm-pwd.txt
    #ssh-passphrase.txt
    #ssh-private-key.txt
    file: secrets/jenkins-passwords
    

Before running the Base Jenkins, we need to ensure secrets get added:

echo 'admin123' > composed/secrets/jenkins-passwords/scm-pwd.txt
echo 'admin123' > composed/secrets/jenkins-passwords/artifacts-pwd.txt

As part of our first run, we will use --force-recreate to ensure that we get a full re-installation each time Jenkins starts.

docker-compose -f composed/docker-compose-jenkins-base.yml up --force-recreate

Jenkins will now be accessible via http://localhost:8080.

Installed and Ready to Work
Configured Credentials
Configured Plugins

Base Image Run Options

Using the Base image and the compose file provided, you do have the option to run this as your primary "always-on" Jenkins instance to use for manual builds, though keep in mind that this may not provide you with the "Infrastructure-as-Code" approach you are looking for. When running as always-on the following should be considered:

  • Setup Security via the Jenkins Configure Global Security
  • Adding another nginx config so you can run this behind our existing NGINX (*.acme.com)
  • Adding Docker Volumes that are currently commented out to ensure that your data is kept during restarts *

* The init-01.groovy will assume that our configuration has been applied when credentials are available to allow restarts while persisting to volume.