OpenDLV
Introduction

Setting up CI+CD using GitLab

As demonstrated in the previous tutorial, all tools for creating, running, and distributing a self-contained environment for the prime checker are available. However, the manual steps with docker save and docker load may perhaps not be the most convenient, as they do not include a formalized distribution channel suitable for cyber-physical systems. Luckily, GitLab contains tools that can be used, both to distribute the Docker images using their Docker registry, but more importantly to automatically create the Docker image whenever a new version of the code repository is present. There are other online services to handle Docker images as well, for example Docker's own Docker hub, but it is, as will now be demonstrated, quite convenient to combine a code repository with an image registry.

Use the GitLab container registry

GitLab call their Docker registry feature GitLab container registry even though it is used for distributing Docker images (rather than containers). The first thing to do is to instruct Docker how to log in to the registry by using the following command:

docker login registry.gitlab.com

You will now be prompted for a user name and password, and these are the ones set for GitLab. When logged in, the credentials will be stored with Docker so that you should not need to log in again. Now, the name of any Docker images that should be pushed to GitLab needs to follow a naming scheme including your username and code repository name. Therefore, continue by recreating the Docker image for the prime checker with a new name by running the following from the source folder:

docker build -t registry.gitlab.com/USERNAME/opendlv-logic-primechecker .

Make sure to change USERNAME into the username you picked at GitLab. Since this was already built before the process will be very quick, as Docker only needs to adjust the name of the resulting image. If you are curious, please run the command docker images to list all images present in your system. Now it is time to push the image to the GitLab registry, and this is done with the following command:

docker push registry.gitlab.com/USERNAME/opendlv-logic-primechecker

Now open the GitLab web page and navigate to the code repository. To the left there is an option in the menu that says Packages and registries and under that an option for Container registry. Open this page to see an entry for the newly pushed image, with the version tag latest. Note that this is now online and it can be pushed from any computer by using the following command:

docker pull registry.gitlab.com/USERNAME/opendlv-logic-primechecker

When doing this, you will get a message stating that the image is up to date, since it is already present on the system as you just created it. To fully test the system, first remove the image from your local system with the following command:

docker rmi -f registry.gitlab.com/USERNAME/opendlv-logic-primechecker

This will completely remove the image from your system, where the -f flag means force used to remove the image even if it is currently used to spawn active containers. Now run the above docker pull command again. This will automatically save the image into Docker, and there is no more need for docker save and docker load as presented earlier. Also note that since you created a private repository, the image can only be downloaded if first logging in using docker login. If the repository is changed to public, then anyone could download and use your prime checker. Next, run the pulled image using the following command:

docker run --rm -ti --net=host registry.gitlab.com/USERNAME/opendlv-logic-primechecker
Tip! If the image referred to in the docker run command does not exist on the system, then Docker will automatically run docker pull on the image first. Therefore, it is rare to manually run docker pull first.

Automate the full CI+CD chain

It is now time to finalize and automate the whole build and deployment process by adding a final piece to the puzzle. Currently there is only once manual step left, and that is to generate the Docker images using docker build and then push them to the image registry using docker push. This can be automated further by letting GitLab run these commands based on triggers from commits to the code repository. In particular, two things can be added: (1) to automatically compile and test the program for every commit that was made to the repository, and (2) to automatically compiled, test, and push a Docker image to the registry for every version tag that is assigned to the code repository. This allows very fine grained control of the build process, where any failed build or test case would automatically alert all software developers when a bad code commit was made, and also at the same time allow for a controlled way of triggering a new Docker image version together with a tag put in the code tree. GitLab allows activation of these features using a special file called .gitlab-ci.yml put in the root of the code repository. Go ahead and open a terminal and go to the folder opendlv-logic-primechecker and create the new file and fill it with the following (gedit .gitlab-ci.yml):

image: docker

services:
  - name: docker:dind

stages:
  - build
  - deploy

before_script:
  - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

build-amd64:
  tags:
    - gitlab-org-docker
  stage: build
  script:
    - docker build .
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

release:
  tags:
    - gitlab-org-docker
  stage: deploy
  script:
    - docker build -t "$CI_REGISTRY_IMAGE":"$CI_COMMIT_TAG" --push .
  rules:
    - if: $CI_COMMIT_TAG =~ /^[0-9.]+$/

The first lines are not so relevant to explain in detail, but in the section stages one can see that the automated build process is divided into two steps, build and deploy. Each stage has a dedicated later section, the first build-amd64 and the second release. The section before_scripts is activated for both stages and has the sole purpose of login into the Docker registry using the internally stored username and password (the variables starting with CI_ are internal GitLab variables used to hide sensitive information). In the rules sections the triggers are given, where the first triggers on a code commit to the default branch (normally main), and the second triggers on a commited tag starting with a number (the strange code to the left is called regular expression and is used to classify text). In the first section, when a commit is made to the default branch, then the command docker build . is executed, and in the second section, when a tag is created, the command docker build -t "$CI_REGISTRY_IMAGE":"$CI_COMMIT_TAG" --push . is run that builds and pushes a Docker image to the registry. The tags section are not very important now, but they indicate what so called Runners to look for inside GitLab's internal computational cluster. Now, save and close the file. Then go ahead and push it to the code repository by using Git as normal:

git status
git add .gitlab-ci.yml
git commit -m "Enabled CI+CD"
git push

When pushed, this commit will directly trigger a build and test of the project as discussed above. Navigate to the GitLab web page and look under CI/CD and then Pipelines to see the running job. You can click on it to get a terminal output from the build, and when everything is done the job will be listed as Passed with green color.

Now, continue by testing the release feature. This is done by creating and pushing a Git tag by using the following commands:

git tag -a 1.0 -m "Version 1.0"
git push origin 1.0

This will create the tag 1.0 locally first, and then the second command pushes it to the remote code repository where the release sequence is triggered. Again check under Pipelines to follow the progress of the job, and when it is completed navigate to the Container registry section to see that there is a new Docker image created and published. The new image can now be started with the following command (try in two different terminals at once):

docker run --rm -ti --net=host registry.gitlab.com/USERNAME/opendlv-logic-primechecker:1.0

It is possible to get an error if running this command, if you happen to use a computer with a different type of CPU than what is used when building the Docker image inside GitLab (amd64). If for example using a computer with an ARM-based CPU, the program will not work (as the compiled machine code always is for a specific target). There is however a solution for this, as presented in the next section.

Adding cross compilation

The CI+CD method as described in the previous section also give great opportunities for building images targeting different types of computers. This is referred to as cross compilation as the GitLab server (using an amd64 type of CPU) builds images that would work on fundamentally different computers.

Docker offers a standardized way of building Docker images for different computers using its docker buildx feature (x as in cross compilation). In fact, the docker buildx feature does not build a single image that works with several machines, but instead a series of images sharing the same name but where only the right sub-variant of the image is sent when the image is requested. The cross compilation feature can easily be integrated into the automated build process, so that every time a tag is added all image variants are prepared. To enable the feature, open the .gitlab-ci.yml file and change it to the following (gedit .gitlab-ci.yml):

image: docker

variables:
  PLATFORMS: "linux/amd64,linux/arm64,linux/arm/v7"

services:
  - name: docker:dind

stages:
  - build
  - deploy

before_script:
  - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

build-amd64:
  tags:
    - gitlab-org-docker
  stage: build
  script:
    - docker build .
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

release:
  tags:
    - gitlab-org-docker
  stage: deploy
  script:
    - docker run --privileged --rm tonistiigi/binfmt --install all
    - docker buildx create --name multiplatformbuilder --use
    - docker buildx build --platform "$PLATFORMS" -t "$CI_REGISTRY_IMAGE":"$CI_COMMIT_TAG" --push .
  rules:
    - if: $CI_COMMIT_TAG =~ /^[0-9.]+$/

The only difference is the script that is run under the release section, and the related variable named $PLATFORMS. The variable is for convenience and simply contains all platforms docker buildx should consider when preparing the image. In the updated script section, the first command enables cross compilation from a tool called QEMU, and the following two lines enables docker buildx with the selected platforms. Save and close the file. Now, create a new release in order to test the cross compilation. Run the following commands:

git status
git add .gitlab-ci.yml
git commit -m "Enabled cross compilation"
git push
git tag -a 1.1 -m "Version 1.1"
git push origin 1.1

When pushed, this commit will directly trigger a new build and test of the project, and then when the tag is pushed a new release for version 1.1 is prepared. Navigate to the GitLab web page and check under Pipelines that the release is successfully prepared, and then try the new version as (in at least two terminals):

docker run --rm -ti --net=host registry.gitlab.com/USERNAME/opendlv-logic-primechecker:1.1