Dockerfile

Goal

This guide analyzes how the Jenkins project automates its Dockerfile file updates.

It’s one thing to publish a Docker image, but it’s another one to maintain it over time.

To scope this guide, we focus on monitoring Java version, and more precisely Java 17.

This guide is heavily inspired by this Jenkins Dockerfile: github.com/jenkinssci/docker

The ultimate goal is to automatically open a pull request such as PR#1944 containing all Java 17 updates.

Requirement

This guide requires:

  1. updatecli

  2. IDE

  3. GitHub Personal Access Token

updatecli

Updatecli is a declarative dependency management tool. The application is available as a command line compatible with Linux/macOS/Windows. Updatecli is designed to work the same way, both from a local machine and a CI environment.

It can easily be installed using Homebrew by running the two following commands:

-> brew tap updatecli/updatecli
-> brew install updatecli

Additional installation instructions are available at www.updatecli.io/docs/prologue/installation.

IDE

The best way to write Updatecli manifest(s) is by using an IDE compatible with jsonschema hosted on schemastore.org, such as Vscode, Intellij, or Neovim. The full list of compatible IDE is available on www.schemastore.org.

For the IDE to load the correct Updatecli jsonschema, Updatecli manifest must have both a parent directory named "updatecli.d" and one of the file extension ".yaml", or ".yml". This provides auto-completion and validation out of the box.

For example, from VScode, typing [ctrl][space] should display a box with various suggestions

GitHub PAT

The GitHub Personal Access Token is the token used to interact with the GitHub API.

In this guide, we use it to clone the repository, create a branch, and open a pull request.

More information on docs.github.com

Pipeline

Description

The pipeline we are analyzing could be schematized as follows:

guide dockerfile

The schema was generated using the following command:

  • updatecli manifest show --config updatecli/updatecli.d/jdk17.yaml --values updatecli/values.yaml --experimental --graph --graph-flavor=mermaid

Manifest

The Updatecli manifest used to generate this pipeline is:

updatecli/updatecli.d/jdk17.yaml
---
name: Bump JDK17 version

scms:
  default:
    kind: github
    spec:
      user: "{{ .github.user }}"
      email: "{{ .github.email }}"
      owner: "{{ .github.owner }}"
      repository: "{{ .github.repository }}"
      token: "{{ requiredEnv .github.token }}"
      username: "{{ .github.username }}"
      branch: "{{ .github.branch }}"

sources:
  lastVersion:
    kind: temurin
    name: Get the latest Adoptium JDK17 version
    spec:
      featureversion: 17
    transformers:
      - trimprefix: "jdk-"

conditions:
  checkTemurinAllReleases:
    name: Check if the "<lastVersion>" is available for all platforms
    kind: temurin
    sourceid: lastVersion
    spec:
      featureversion: 17
      platforms:
        - alpine-linux/x64
        - linux/x64
        - linux/aarch64
        - linux/ppc64le
        - linux/s390x
        - windows/x64

targets:
  ## Global config files
  setJDK17VersionDockerBake:
    name: "Bump JDK17 version for Linux images in the docker-bake.hcl file"
    kind: hcl
    transformers:
      - replacer:
          from: "+"
          to: "_"
    spec:
      file: docker-bake.hcl
      path: variable.JAVA17_VERSION.default
    scmid: default
  setJDK17VersionWindowsDockerCompose:
    name: "Bump JDK17 version in build-windows.yaml"
    kind: yaml
    transformers:
      - replacer:
          from: "+"
          to: "_"
    spec:
      files:
        - build-windows.yaml
      key: $.services.jdk17.build.args.JAVA_VERSION
    scmid: default
  ## Dockerfiles
  # Setting default JAVA_VERSION ARG to current Jenkins default JDK17
  setJDK17VersionAlpine:
    name: "Bump JDK17 version for Linux images in the Alpine Linux Dockerfile"
    kind: dockerfile
    transformers:
      - replacer:
          from: "+"
          to: "_"
    spec:
      file: alpine/hotspot/Dockerfile
      instruction:
        keyword: ARG
        matcher: JAVA_VERSION
    scmid: default
  setJDK17VersionDebian:
    name: "Bump JDK17 version for Linux images in the Debian Dockerfiles"
    kind: dockerfile
    transformers:
      - replacer:
          from: "+"
          to: "_"
    spec:
      files:
        - debian/bookworm/hotspot/Dockerfile
        - debian/bookworm-slim/hotspot/Dockerfile
      instruction:
        keyword: ARG
        matcher: JAVA_VERSION
    scmid: default
  setJDK17VersionRhel:
    name: "Bump JDK17 version for Linux images in the Rhel Dockerfile"
    kind: dockerfile
    transformers:
      - replacer:
          from: "+"
          to: "_"
    spec:
      file: rhel/ubi9/hotspot/Dockerfile
      instruction:
        keyword: ARG
        matcher: JAVA_VERSION
    scmid: default
  setJDK17VersionWindowsDockerImage:
    name: "Bump default JDK17 version for Linux images in the Windows Dockerfile"
    kind: dockerfile
    transformers:
      - replacer:
          from: "+"
          to: "_"
    spec:
      file: windows/windowsservercore/hotspot/Dockerfile
      instruction:
        keyword: ARG
        matcher: JAVA_VERSION
    scmid: default

actions:
  default:
    kind: github/pullrequest
    scmid: default
    title: Bump JDK17 version to {{ source "lastVersion" }}
    spec:
      labels:
        - dependencies
        - jdk17
updatecli/values.yaml
# Values file
github:
  user: "GitHub Actions"
  email: "41898282+github-actions[bot]@users.noreply.github.com"
  username: "github-actions"
  token: "UPDATECLI_GITHUB_TOKEN"
  branch: "master"
  owner: "jenkinsci"
  repository: "docker"

Then you can run:

export UPDATECLI_GITHUB_TOKEN=<your_PAT>
updatecli pipeline diff --config updatecli/updatecli.d/jdk17.yaml --values updatecli/values.yaml

This manifest named Bump JDK17 version can be split into five sections:

  1. scms

  2. sources

  3. conditions

  4. targets

  5. actions

SCMS
updatecli/updatecli.d/jdk17.yaml
scms:
  default:
    kind: github
    spec:
      user: "{{ .github.user }}"
      email: "{{ .github.email }}"
      owner: "{{ .github.owner }}"
      repository: "{{ .github.repository }}"
      token: "{{ requiredEnv .github.token }}"
      username: "{{ .github.username }}"
      branch: "{{ .github.branch }}"

The scm section defines the git repositories to interact with, either to retrieve information or to update them.

The scm section accepts different kinds of configuration such as git, gitea, gitlab, etc.

In the current scenario, we need to update files on a GitHub repository so we use the scm of kind github. We could also have used the kind git, but as you’ll discover later in this guide, we need to leverage specific GitHub behaviour such as GitHub pull request.

Worth mentioning, most of the time we keep the scm settings parametrised so we can easily override all pipeline values. For example, to test the workflow against a different Git repository.

For that we use a file named values.yaml with different settings. The token key holds the name of the environment variable to read, which is why the manifest uses requiredEnv .github.token rather than a hardcoded variable name.

updatecli/values.yaml
# Values file
github:
  user: "GitHub Actions"
  email: "41898282+github-actions[bot]@users.noreply.github.com"
  username: "github-actions"
  token: "UPDATECLI_GITHUB_TOKEN"
  branch: "master"
  owner: "jenkinsci"
  repository: "docker"

You could easily replace in the file values.yaml the owner and repository values to point to your fork.

Then you can run:

  • Dry run mode: updatecli pipeline diff --config updatecli/updatecli.d/jdk17.yaml --values updatecli/values.yaml

  • Apply mode: updatecli pipeline apply --config updatecli/updatecli.d/jdk17.yaml --values updatecli/values.yaml

Sources
updatecli/updatecli.d/jdk17.yaml
sources:
  lastVersion:
    kind: temurin
    name: Get the latest Adoptium JDK17 version
    spec:
      featureversion: 17
    transformers:
      - trimprefix: "jdk-"

The sources section defines how to retrieve source information. There are many different kinds of source like dockerimage, dockerdigest, etc, where each plugin relies on different parameters specified within the spec section. In this case we decide to use a source of kind temurin which allows us to retrieve a specific Temurin version.

The first step here is to identify the "latest" version for Java 17.

Then to get the information we are looking for, we have to "transform" the retrieved version, using a set of transformers rules, in this example we don’t need the prefix "jdk-" as all we need is the version number.

Conditions
updatecli/updatecli.d/jdk17.yaml
conditions:
  checkTemurinAllReleases:
    name: Check if the "<lastVersion>" is available for all platforms
    kind: temurin
    sourceid: lastVersion
    spec:
      platforms:
        - alpine-linux/x64
        - linux/x64
        - linux/aarch64
        - linux/ppc64le
        - linux/s390x
        - windows/x64

Quite often, before updating a file, we want to run early checks to ensure that the updates are relevant. Dockerfile is no exception. We want to be sure that we’ll still be able to build our Docker image after the updates, otherwise it makes no sense to update our Dockerfile.

Once again, there are many things we would like to test, in this case we want to be sure that the Temurin version is available for all the architectures we need to build a Docker image for.

This example relies on the parameter sourceid to define what source output will be used to fetch the default Temurin version we need to check.

Targets

The targets section defines the files to monitor for update.

We can have as many targets as we want. Our manifest contains six targets, but only three of them are detailed here.

Each target with the same scmid will create one Git commit, all of them landing in the same pull request as defined later by our action.

Dockerfile
updatecli/updatecli.d/jdk17.yaml
targets:
  setJDK17VersionWindowsDockerImage:
    name: "Bump default JDK17 version for Linux images in the Windows Dockerfile"
    kind: dockerfile
    transformers:
      - replacer:
          from: "+"
          to: "_"
    spec:
      file: windows/windowsservercore/hotspot/Dockerfile
      instruction:
        keyword: ARG
        matcher: JAVA_VERSION
    scmid: default

The setJDK17VersionWindowsDockerImage target updates the Dockerfile ARG JAVA_VERSION for the Windows Server Core image. Since we only defined one source in the manifest, Updatecli uses that source output as the default entry for this target.

The only subtlety here is that our source output contains "+" in the version number, and we need to replace it by "_", so we use a transformer of kind replacer to do so. Please note that a transformer defined in a target is only applied to this target, while a transformer defined in a source is applied to all targets using this source.

The scmid is set to default so we know that this target will monitor the file "windows/windowsservercore/hotspot/Dockerfile" in the GitHub repository defined by the scm configuration.

YAML
updatecli/updatecli.d/jdk17.yaml
targets:
  setJDK17VersionWindowsDockerCompose:
    name: "Bump JDK17 version in build-windows.yaml"
    kind: yaml
    transformers:
      - replacer:
          from: "+"
          to: "_"
    spec:
      files:
        - build-windows.yaml
      key: $.services.jdk17.build.args.JAVA_VERSION
    scmid: default

The setJDK17VersionWindowsDockerCompose target updates the JDK17 version in the build-windows.yaml file. Instead of using the plugin dockerfile, we use the plugin yaml to update the version number in the build-windows.yaml file. Once again, we need to replace the "+" character by "_" in the version number.

HCL
updatecli/updatecli.d/jdk17.yaml
targets:
  ## Global config files
  setJDK17VersionDockerBake:
    name: "Bump JDK17 version for Linux images in the docker-bake.hcl file"
    kind: hcl
    transformers:
      - replacer:
          from: "+"
          to: "_"
    spec:
      file: docker-bake.hcl
      path: variable.JAVA17_VERSION.default
    scmid: default

The setJDK17VersionDockerBake target updates the JDK17 version in the docker-bake.hcl file consumed by Docker Buildx Bake. Instead of using the plugin dockerfile, we use the plugin hcl to update the version number in the docker-bake.hcl file.

Actions
updatecli/updatecli.d/jdk17.yaml
actions:
  default:
    kind: github/pullrequest
    scmid: default
    title: Bump JDK17 version to {{ source "lastVersion" }}
    spec:
      labels:
        - dependencies
        - jdk17

An action is executed when at least one target is modified. Once again there are many different kinds of action but in this case we are leveraging the action of kind github/pullrequest that must be bound to a scm using the parameter scmid. So we are telling Updatecli to open a pull request on the GitHub repository jenkinsci/docker and to assign the labels dependencies and jdk17.

It is worth mentioning that by default, the GitHub scm will do all its operations from a temporary Git branch named updatecli_main_xxx, so the pull request will be opened from this branch.

Going further

Once you’re happy with the Update pipeline, you can use your CI environment to run it regularly. That’s what we do on the Jenkins project where we run this pipeline every Monday github.com/jenkinsci/docker

You’ll need updatecli installed in your CI environment, and the right credentials. More information:

This guide demonstrated a more advanced update pipeline. You can find similar Updatecli pipelines on the Jenkins project github.com/jenkinsci/docker

We are curious about your advanced pipelines, so feel free to share them with us.

Feel free to chat with us on the Updatecli Matrix channel.