<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Bash |</title><link>https://kristianeschenburg.netlify.app/tag/bash/</link><atom:link href="https://kristianeschenburg.netlify.app/tag/bash/index.xml" rel="self" type="application/rss+xml"/><description>Bash</description><generator>Source Themes Academic (https://sourcethemes.com/academic/)</generator><language>en-us</language><lastBuildDate>Tue, 02 May 2023 02:14:14 -0700</lastBuildDate><image><url>https://kristianeschenburg.netlify.app/img/Bayes.jpg</url><title>Bash</title><link>https://kristianeschenburg.netlify.app/tag/bash/</link></image><item><title>CI/CD Part 1: Gitlab Pipelines and Package Registries</title><link>https://kristianeschenburg.netlify.app/post/cicd-1-pipelines-and-packages/</link><pubDate>Tue, 02 May 2023 02:14:14 -0700</pubDate><guid>https://kristianeschenburg.netlify.app/post/cicd-1-pipelines-and-packages/</guid><description>&lt;p>I recently developed a template workflow to help our team adopt a CI/CD-based development strategy. Many of our web applications and tools were based on simple repository structures. With growing datasets and ever-increasing use by outside teams, we found ourselves needing to add new features more frequently to many of these tools and believed that continuous integration and deployment could help us not just develop more quickly, but also more intelligently. Since we use Gitlab to store our code, we decided to use the Gitlab CI/CD tools.&lt;/p>
&lt;p>Documentation on much of this process was scattered and/or sparse, so I decided to put what I learned and implemented into a more coherent set of notes. This post covers the anatomy of a &lt;code>.gitlab-ci.yml&lt;/code> file, how jobs and stages fit together, how to make a job conditional, and how to authenticate against a package registry so a pipeline can build a Python package and push it there.&lt;/p>
&lt;p>The
&lt;a href="https://kristianeschenburg.netlify.app/post/cicd-2-images-and-registries/">second post&lt;/a> covers the other half: writing a multi-stage Dockerfile to build an image from that package, and pushing the image to a container registry.&lt;/p>
&lt;h2 id="setup">Setup&lt;/h2>
&lt;p>I did all of my testing using my personal Gitlab account. To separate things out, I created a new Project called &amp;ldquo;Package Registry”, as well as a test repository that was used for building a local Python project called &amp;ldquo;TemplateCI&amp;rdquo;.&lt;/p>
&lt;p>The &amp;ldquo;Package Registry&amp;rdquo; Project serves as just that &amp;ndash; an all-inclusive location for any software packages your CI/CD pipelines build. You can find the built packages by clicking &lt;strong>${Project Name} &amp;gt; Deploy &amp;gt; Package Registry&lt;/strong>. Every &amp;ldquo;Project&amp;rdquo; in Gitlab has the ability to store packages in its own registry, but I felt it cleaner to store everything in one repo. Similarly, the actual code that I&amp;rsquo;ll be packaging will be stored in the &amp;ldquo;TemplateCI&amp;rdquo; repo.&lt;/p>
&lt;h2 id="authentication-pypirc-and-netrc">Authentication: .pypirc and .netrc&lt;/h2>
&lt;p>In order to build packages and push them to a remote package registry, we use the &lt;code>build&lt;/code> and &lt;code>twine&lt;/code> packages. &lt;code>build&lt;/code> generates a package, and &lt;code>twine&lt;/code> pushes this package to a registry (or &amp;ldquo;index&amp;rdquo;). &lt;code>twine&lt;/code> requires access to authentication usernames, passwords, and a registry URL in order to do so. &lt;code>twine&lt;/code> can access these tokens from a &lt;code>.pypirc&lt;/code> file &amp;ndash; the tokens are generated by the registry, and ensure that the submitting user has permissions to perform a certain action.&lt;/p>
&lt;p>Other processes, such as pulling or pushing code from a remote repository, often require additional usernames and passwords. In order to alleviate the need to consistently provide these variables at request time, we can save them in a &lt;code>.netrc&lt;/code> file.&lt;/p>
&lt;p>These are straightforward to set up locally. But we also need to set these up to ensure a properly functional CI/CD workflow. I&amp;rsquo;ve put together a basic script, called &lt;code>setup_tokens.sh&lt;/code> that does just that:&lt;/p>
&lt;h3 id="setup_tokenssh">setup_tokens.sh&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># generate .pypirc file&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;[distutils]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">index-servers =
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> personal
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">[personal]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">repository = https://gitlab.com/api/v4/projects/&lt;/span>&lt;span class="nv">$PACKAGE_REGISTRY_ID&lt;/span>&lt;span class="s2">/packages/pypi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">username = &lt;/span>&lt;span class="nv">$CI_DEPLOY_USER&lt;/span>&lt;span class="s2">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">password = &lt;/span>&lt;span class="nv">$CI_DEPLOY_PASSWORD&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &amp;gt; ~/.pypirc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># generate .netrc file&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;machine gitlab.com
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">login gitlab-ci-token
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">password &lt;/span>&lt;span class="nv">$CI_JOB_TOKEN&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &amp;gt; ~/.netrc&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>.pypirc&lt;/code> refers to your Project Registry via a previously generated authentication token and password, and allows your to build and upload Python packages to that registry.&lt;/p>
&lt;p>The &lt;code>.netrc&lt;/code> file enables you to pull private packages from that same registry. In the context of our work, we&amp;rsquo;ll want to build and push packages to the registry first so that they are available for pulling. For example, in the &lt;code>Pipfile&lt;/code> for this template project, we have the following:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[[&lt;/span>&lt;span class="nx">source&lt;/span>&lt;span class="p">]]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">url&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;https://pypi.org/simple&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">verify_ssl&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">name&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;pypi&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[[&lt;/span>&lt;span class="nx">source&lt;/span>&lt;span class="p">]]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">url&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;https://${CI_DEPLOY_USER}:${CI_DEPLOY_PASSWORD}@gitlab.com/api/v4/projects/${$PACKAGE_REGISTRY_ID}/packages/pypi/simple&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">verify_ssl&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">name&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;personal&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We see the same user authentication happening, along with the reference to the Package Registry ID variable. For local installation of your package, and in order to make sure that your &lt;code>Pipfile&lt;/code> and &lt;code>Pipfile.lock&lt;/code> are in sync, you&amp;rsquo;ll need to define the following local environment variables:&lt;/p>
&lt;ul>
&lt;li>&lt;code>CI_DEPLOY_USER&lt;/code>: generated user token&lt;/li>
&lt;li>&lt;code>CI_DEPLOY_PASSWORD&lt;/code>: generated token password&lt;/li>
&lt;li>&lt;code>PACKAGE_REGISTRY_ID&lt;/code>: the ID of the repository that you created that will store your packages&lt;/li>
&lt;/ul>
&lt;h2 id="basic-jobs">Basic jobs&lt;/h2>
&lt;p>The basis of a Gitlab CI/CD pipeline is the &lt;code>.gitlab-ci.yml&lt;/code> file, which is composed of a set of explicitly-defined and temporally-ordered “stages”. A stage is composed of a set of “jobs”. Jobs are the workhorses of the CI/CD pipeline, and define explicit tasks that a CI/CD pipeline runs. By default, all jobs from one stage run in parallel, unless specified otherwise (using the &lt;code>needs&lt;/code> keyword as an attribute of a job induces a temporal directed acyclic graph – jobs can be made “dependent” on the successful completion of other jobs within a stage). In this example, I’ve defined three stages:&lt;/p>
&lt;ol>
&lt;li>run-unit-tests&lt;/li>
&lt;li>build-package&lt;/li>
&lt;li>build-image&lt;/li>
&lt;/ol>
&lt;p>Any job associated with the &lt;code>run-unit-tests&lt;/code> stage will run to completion (or failure) PRIOR TO ANY job in the stages &lt;code>build-package&lt;/code> and &lt;code>build-image&lt;/code> starting. If all jobs in the &lt;code>run-unit-tests&lt;/code> stage complete successfully, then the next stage (&lt;code>build-package&lt;/code>) will begin. We define individual jobs, and give them a stage attribute. Stages define the rough ordering of jobs. Each job runs in the context of an “image” or environment. We can set a global &lt;code>image&lt;/code> and define stages as&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># global CI/CD image&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">python:3.9-slim&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># stages of this example pipeline&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">stages&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">run-unit-tests&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">build-package&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">build-image&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">variables&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">LC_ALL&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">C.UTF-8&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">LANG&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">C.UTF-8&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>or define the image as an attribute of a job. Gitlab CI/CD by default uses Docker images in which to run jobs. Setting the image is analogous to using the &lt;code>FROM&lt;/code> command in a Dockerfile. We&amp;rsquo;ve also set some global variables, here the &lt;code>LC_ALL&lt;/code> and &lt;code>LANG&lt;/code> variables.&lt;/p>
&lt;p>To “run” Gitlab pipelines for the purpose of CI/CD, we use “runners”, which are build instances installed on a server. Gitlab offers “shared” runners (use of these is free if you use
&lt;a href="https://www.gitlab.com" target="_blank" rel="noopener">www.gitlab.com&lt;/a>, but you need to register a credit card to prevent abuse of Gitlab resources). You can also register your own device(s) to act as a Gitlab runner.&lt;/p>
&lt;p>Below are examples of two jobs in the &lt;code>run-unit-tests&lt;/code> stage. These two jobs are effectively the same code, apart from the unique unit tests that they run. However, we&amp;rsquo;ve made the job &lt;code>unit-tests-2&lt;/code> dependent on the output of the job &lt;code>unit-tests-1&lt;/code> (see the &lt;code>needs&lt;/code> keyword of &lt;code>unit-tests-2&lt;/code>). Both jobs use the global &lt;code>python:3.9-slim&lt;/code> image. We can run some “setup” stuff (&lt;code>before_script&lt;/code>), run an actual script (&lt;code>script&lt;/code>), and run clean up (&lt;code>after_script&lt;/code>, not shown) &amp;ndash; these delineations (before, during, after) are for organizational purposes, and not due to any explicit functional differences in the delineations.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># setup_tokens.sh is the script from the previous section.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># example job #1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">unit-tests-1&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">stage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">run-unit-tests&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">before_script&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">chmod +x ./setup_tokens.sh; ./setup_tokens.sh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">python3 -m pip install pipenv&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">apt-get update&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">apt-get install --yes --no-install-recommends gcc g++ libffi-dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">python3 -m pipenv install --deploy --dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># run first set of unit tests&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">python3 -m pipenv run pytest -k &amp;#39;test_examples1.py&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># example job #2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">unit-tests-2&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">stage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">run-unit-tests&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">before_script&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">chmod +x ./setup_tokens.sh; ./setup_tokens.sh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">python3 -m pip install pipenv&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">apt-get update&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">apt-get install --yes --no-install-recommends gcc g++ libffi-dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">python3 -m pipenv install --deploy --dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># run second set of unit tests&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">python3 -m pipenv run pytest -k &amp;#39;test_examples2.py&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># wait for job 1 to finish&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">needs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">unit-tests-1]&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="conditional-pipeline-jobs">Conditional pipeline jobs&lt;/h2>
&lt;p>The above jobs are relatively simple and will run every time you push a repository to Gitlab. However, sometimes, we might only want to run a job if certain conditions are met. For example, we might only want to build a package from the &lt;code>main&lt;/code> branch, or only after a merge request is made. To this end, we can add “rules” to a job that restrict when it is actually run.&lt;/p>
&lt;p>Below is a more complicated job example. The overarching goal of this job is to build a Docker image from a local Python project and push the image to the Gitlab Container Registry. There’s a lot going on here, so I’ll break it up into pieces, but here is the whole job:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Conditional job&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># Building a docker image and pushing this container to a container registry&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># Link to main image: https://github.com/bentolor/docker-dind-awscli&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># Conditions:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># --- merge request events&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># --- target branch of merge request is &amp;#34;main&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># job name&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">build-image-glcr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># stage of pipeline&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">stage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">build-image&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># image that job is based on&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docker:20.10.16&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># sub-services of job&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">docker:20.10.16-dind&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># variables available to the job&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">variables&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">DOCKER_TLS_CERTDIR&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;/certs&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">IMAGE_TAG&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># some setup scripts -- here, just making sure docker is available&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">before_script&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">docker info&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># meat of the job -- authentication, building, run, push&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">docker build &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>--&lt;span class="l">build-arg CI_DEPLOY_USER=$CI_DEPLOY_USER &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>--&lt;span class="l">build-arg CI_DEPLOY_PASSWORD=$CI_DEPLOY_PASSWORD &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>--&lt;span class="l">build-arg CI_JOB_TOKEN=$CI_JOB_TOKEN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>-&lt;span class="l">t $IMAGE_TAG .&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">docker run $IMAGE_TAG&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">docker push $IMAGE_TAG&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># job conditions&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">$CI_PIPELINE_SOURCE == &amp;#39;merge_request_event&amp;#39; &amp;amp;&amp;amp; $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == &amp;#34;main&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This job is associated with a new stage called &lt;code>build-image&lt;/code> that will run as the last stage of this example CI/CD pipeline. Without going into the specifics of the
&lt;a href="https://kristianeschenburg.netlify.app/post/cicd-2-images-and-registries/">Dockerfile&lt;/a> just yet, this stage builds and pushes a Docker image to a remote repository. We defined the job image as &lt;code>docker:20.10.16&lt;/code> and an additional “service” attribute as &lt;code>docker:20.10.16-dind&lt;/code> where “dind” means “Docker-in-Docker”. The Docker-in-Docker feature allows an image to run Docker itself (pretty meta, huh). The idea here is to instantiate a job from a specific Docker container (“image”) which itself has Docker installed (“dind”), which will allow the docker:20.10.16 image to build &lt;em>another&lt;/em> Docker container. We’ve also defined some variables:&lt;/p>
&lt;ul>
&lt;li>&lt;code>DOCKER_TLS_CERTDIR&lt;/code>: needed to allow the larger scale image to communicate with a service (honestly, I don’t quite understand this and documentation on CI/CD &amp;ldquo;services&amp;rdquo; is sparse)&lt;/li>
&lt;li>&lt;code>IMAGE_TAG&lt;/code>: refers to the container destination and the image “name” &amp;ndash; this just makes our lives easier by turning into a variable what would otherwise be a really long string&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c">#### Build Docker image and push to Gitlab Container Registry&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">build-image-glcr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">stage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">build-image&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docker:20.10.16&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">docker:20.10.16-dind&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">variables&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">DOCKER_TLS_CERTDIR&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;/certs&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">IMAGE_TAG&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Next up, we have all the script stuff. You’ll probably recognize most of the &lt;code>docker ${command}&lt;/code> commands. We first “authenticate” the current job with the Gitlab container registry (apparently there are a variety of ways to “authenticate” with Docker using one set of variables or another – the way below is the way I was able to get working, but there are
&lt;a href="https://stackoverflow.com/questions/61251622/how-to-authenticate-to-gitlabs-container-registry-before-building-a-docker-imag" target="_blank" rel="noopener">other solutions&lt;/a>). We then build the Docker image, run the image, and push the image to the container registry.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">script&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">docker build &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>--&lt;span class="l">build-arg CI_DEPLOY_USER=$CI_DEPLOY_USER &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>--&lt;span class="l">build-arg CI_DEPLOY_PASSWORD=$CI_DEPLOY_PASSWORD &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>--&lt;span class="l">build-arg CI_JOB_TOKEN=$CI_JOB_TOKEN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>-&lt;span class="l">t $IMAGE_TAG .&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">docker run $IMAGE_TAG&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">docker push $IMAGE_TAG&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You’ll notice a bunch of variables that I didn&amp;rsquo;t explicitly define anywhere:&lt;/p>
&lt;ul>
&lt;li>&lt;code>CI_REGISTRY_USER&lt;/code>: username for project (I think we can also use &lt;code>CI_DEPLOY_USER&lt;/code>, though it looks like there might be some things to figure out with branch / variable protections / non-protections)&lt;/li>
&lt;li>&lt;code>CI_REGISTRY_PASSWORD&lt;/code>: defaults to &lt;code>CI_JOB_TOKEN&lt;/code> value (this value is ephemeral e.g. valid only for one job at a time I think? I think we can also use the &lt;code>CI_DEPLOY_PASSWORD&lt;/code> for a longer-lived alternative, though it looks like there might be some things to figure out with branch / variable protections / non-protections)&lt;/li>
&lt;li>&lt;code>CI_REGISTRY&lt;/code>: defaults to &lt;code>https://gitlab.com/${group}/${project-name}/container_registry&lt;/code>&lt;/li>
&lt;li>&lt;code>CI_DEPLOY_USER&lt;/code>: generated user token&lt;/li>
&lt;li>&lt;code>CI_DEPLOY_PASSWORD&lt;/code>: generated token password&lt;/li>
&lt;li>&lt;code>CI_JOB_TOKEN&lt;/code>: see documentation
&lt;a href="https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html" target="_blank" rel="noopener">here&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>These are all
&lt;a href="https://docs.gitlab.com/ee/ci/variables/predefined_variables.html" target="_blank" rel="noopener">“predefined” variables&lt;/a>, meaning they already exist in the Gitlab CI/CD context as part of having a
&lt;a href="https://www.gitlab.com" target="_blank" rel="noopener">www.gitlab.com&lt;/a> account, without you explicitly defining them. However, I did run into some issues using &lt;code>CI_DEPLOY_USER&lt;/code> and &lt;code>CI_DEPLOY_PASSWORD&lt;/code>. In addition to having predefined variables provided by Gitlab CI/CD, we can also
&lt;a href="./docs/SettingEnvVariables.md">&lt;em>manually&lt;/em> predefine variables&lt;/a> for a whole Gitlab Project or for a whole Group. Go to the page for your &lt;strong>Project/Group &amp;gt; Settings &amp;gt; CI/CD &amp;gt; Variables &amp;gt; Expand&lt;/strong>. For example, I defined the &lt;code>CI_DEPLOY_USER&lt;/code> and &lt;code>CI_DEPLOY_PASSWORD&lt;/code> variables for my Group. These variables are the Group-level authentication tokens and are now made accessible to all Gitlab CI/CD jobs running under this Group. Additionally, although not shown here (because it&amp;rsquo;s used within the &lt;code>setup_tokens.sh&lt;/code> script), we&amp;rsquo;ve also defined an environment variable called &lt;code>PACKAGE_REGISTRY_ID&lt;/code> that tells the CI/CD pipeline where to build and push packages.&lt;/p>
&lt;p>And finally, what we’ve been waiting for, conditional pipeline jobs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">$CI_PIPELINE_SOURCE == &amp;#39;merge_request_event&amp;#39; &amp;amp;&amp;amp; $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == &amp;#34;main&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>As part of this job, I&amp;rsquo;ve defined a rule that indicates that this job should &lt;strong>only&lt;/strong> be run 1) upon a merge request event (i.e. if you click “Create Merge Request” in the console), and 2) if the name of the target branch of that merge request event is &lt;code>main&lt;/code>. For example, if I create a new branch called &lt;code>dev&lt;/code> off of &lt;code>main&lt;/code> and create a merge request in the Gitlab console, this job will run. However, it’s important to note that, if you push a commit to the &lt;code>dev&lt;/code> branch &lt;em>after&lt;/em> creating the merge request, this job will &lt;em>still&lt;/em> run, even if you haven’t created a &lt;em>new&lt;/em> merge request e.g. &lt;code>CI_PIPELINE_SOURCE&lt;/code> == &amp;ldquo;merge_request_event&amp;rdquo; will always default to &lt;code>TRUE&lt;/code> after the first merge request event, as long as the source branch is still actively being developed. Additionally, the job itself for this &lt;strong>will run the source branch code, not the target branch code&lt;/strong>. For Gitlab Premium users, there is an additional criterion called a “merged_result_event”, which would run the target branch (&lt;code>main&lt;/code>) code after merging the source branch (&lt;code>dev&lt;/code>) into the target branch.&lt;/p>
&lt;h2 id="building-and-pushing-a-package">Building and pushing a package&lt;/h2>
&lt;p>Building a package locally is straightforward, but doing so within a Gitlab CI/CD pipeline is a little more complicated. But, we can imagine adding this type of task to a CI/CD pipeline, and conditioning it on a merge request (or something of that kind).&lt;/p>
&lt;p>I&amp;rsquo;ve called this job &lt;code>build-package&lt;/code> and it belongs to a stage also called &lt;code>build-package&lt;/code>. We first set up the &lt;code>.pypirc&lt;/code> and &lt;code>.netrc&lt;/code> files in image running the job, and then install the &lt;code>build&lt;/code> and &lt;code>twine&lt;/code> libraries in the &lt;code>before_script&lt;/code> attribute. Then, using the &lt;code>script&lt;/code> attribute, we build our package, and push it to our package registry (we&amp;rsquo;ve defined the registry in our &lt;code>.pypirc&lt;/code> file &amp;ndash; here, it&amp;rsquo;s referred to as the &amp;ldquo;personal&amp;rdquo; registry.)&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">python:3.9-slim&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">variables&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">PACKAGE_REGISTRY_NAME&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;personal&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">stages&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">build-package&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c">#### BUILDING PACKAGE AND PUSHING TO GITLAB PACKAGE REGISTRY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">build-package&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">stage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">build-package&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">before_script&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">chmod +x ./setup_tokens.sh; ./setup_tokens.sh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">apt-get update&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">apt-get install --yes --no-install-recommends gcc g++ libffi-dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">python3 -m pip install build twine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">python3 -m build&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">python3 -m twine upload --repository ${PACKAGE_REGISTRY_NAME} dist/* --verbose&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Some of the (many) external links I used in this process:&lt;/p>
&lt;ul>
&lt;li>
&lt;a href="https://docs.gitlab.com/ee/ci/variables/predefined_variables.html" target="_blank" rel="noopener">Predefined&lt;/a> CI/CD variables&lt;/li>
&lt;li>
&lt;a href="https://gitlab.com/gitlab-org/gitlab/-/issues/214014" target="_blank" rel="noopener">Adding&lt;/a> variables to the Gitlab CI/CD context&lt;/li>
&lt;li>
&lt;a href="https://gitlab.com/gitlab-org/gitlab/-/issues/350582" target="_blank" rel="noopener">Setting&lt;/a> up a ~/.netrc file with Gitlab credentials&lt;/li>
&lt;li>
&lt;a href="https://stackoverflow.com/questions/72789599/gitlab-ci-cd-execute-script-file-that-exist-in-the-repository" target="_blank" rel="noopener">Executing&lt;/a> a bash scripting within a Gitlab CI/CD pipeline&lt;/li>
&lt;li>
&lt;a href="https://stackoverflow.com/questions/58939500/how-to-pass-gitlab-ci-file-variable-to-dockerfile-and-docker-container" target="_blank" rel="noopener">Passing&lt;/a> variables to Docker image at build time&lt;/li>
&lt;li>
&lt;a href="https://www.shellhacks.com/gitlab-ci-cd-build-docker-image-push-to-registry/" target="_blank" rel="noopener">Building&lt;/a> Docker image and pushing to registry&lt;/li>
&lt;li>
&lt;a href="https://docs.gitlab.com/ee/user/packages/container_registry/authenticate_with_container_registry.html" target="_blank" rel="noopener">Authenticating&lt;/a> container registries&lt;/li>
&lt;li>
&lt;a href="https://docs.gitlab.com/ee/user/packages/pypi_repository/#authenticate-with-the-package-registry" target="_blank" rel="noopener">Setting&lt;/a> up a ~/.pypirc files with Gitlab credentials&lt;/li>
&lt;/ul></description></item><item><title>Submitting Batch Jobs with qsub</title><link>https://kristianeschenburg.netlify.app/post/submitting-batch-jobs-with-qsub/</link><pubDate>Tue, 05 May 2020 14:24:17 -0700</pubDate><guid>https://kristianeschenburg.netlify.app/post/submitting-batch-jobs-with-qsub/</guid><description>&lt;p>I&amp;rsquo;m fortunate enought to work in a lab with some high-level computing infrastructure. We have a cluster of machines using the
&lt;a href="http://bioinformatics.mdc-berlin.de/intro2UnixandSGE/sun_grid_engine_for_beginners/README.html" target="_blank" rel="noopener">Sun Grid Engine&lt;/a> (SGE) software system for distributed resource management. The other day, I was searching for how to wrap my Python scripts with &lt;code>qsub&lt;/code> so that I could submit a batch of jobs to our cluster. Eventually, I want to be able to submit jobs with dependencies between them, but we&amp;rsquo;ll start here.&lt;/p>
&lt;p>Let&amp;rsquo;s create an example script that computes the mean of an MRI image. Let&amp;rsquo;s call the script &lt;code>compute_mean.py&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="ch">#!/usr/bin/env python&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">argparse&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">nibabel&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">nb&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">numpy&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">np&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">pandas&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">pd&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">parser&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ArgumentParser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Compute the mean of MRI, and save to CSV file.&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">parser&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add_argument&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;-i&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;--input_image&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">help&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;Path to MRI image.&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">required&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">str&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">parser&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add_argument&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;-o&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;--output_csv&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">help&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;Output CSV file.&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">required&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">str&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">args&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">parser&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">parse_args&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># read in image&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">img&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">nb&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">load&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">input_image&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># get voxel-wise data&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">img&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get_data&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># compute mean&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mu&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">np&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mean&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># save to csv&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">df&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">DataFrame&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="s1">&amp;#39;mean&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">mu&lt;/span>&lt;span class="p">]})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">df&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">to_csv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">output_csv&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The first line of this script, &lt;code>#!/usr/bin/env python&lt;/code> tells the script to use the local &lt;code>python&lt;/code> environment. In my case, I have a customized installation of Python, along with a bunch packages and libraries that I&amp;rsquo;ve written and installed that are not available for the rest of my lab (since they&amp;rsquo;re still in the testing phase or just something I&amp;rsquo;m experimenting with). This line tells the script to use &lt;em>my&lt;/em> Python environment, rather than the default version on our servers.&lt;/p>
&lt;p>We can then create a bash wrapper, let&amp;rsquo;s call in &lt;code>mean_wrapper.sh&lt;/code> for a single subject&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>&lt;span class="c1">#$ -M keschenb@uw.edu&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#$ -m abe&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#$ -r y&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#$ -o tmp.out&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#$ -e tmp.err&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Compute mean of image&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">image&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">$1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">output&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">$2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python compute_mean.py &lt;span class="si">${&lt;/span>&lt;span class="nv">image&lt;/span>&lt;span class="si">}&lt;/span> &lt;span class="si">${&lt;/span>&lt;span class="nv">output&lt;/span>&lt;span class="si">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The second and third line here, with the &lt;code>M&lt;/code> and &lt;code>m&lt;/code> parameters, tell the script to email me once it completes the processing (or if there are any errors). And finally, we can create a wrapper that takes in a list of subjects to process, and the input and output directories, and submits each individual job to the queue using &lt;code>qsub&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">subjects&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">$1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">image_dir&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">$2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">output_dir&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">$3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># we create a variable, as our cluster has 2 different queues to use&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># this could be hardcoded though&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">queue_name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">$4&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">while&lt;/span> &lt;span class="nb">read&lt;/span> subj
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">image_file&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">image_dir&lt;/span>&lt;span class="si">}${&lt;/span>&lt;span class="nv">subj&lt;/span>&lt;span class="si">}&lt;/span>.nii.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">output_file&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">output_dir&lt;/span>&lt;span class="si">}${&lt;/span>&lt;span class="nv">subj&lt;/span>&lt;span class="si">}&lt;/span>.csv
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> qsub -q &lt;span class="si">${&lt;/span>&lt;span class="nv">queue_name&lt;/span>&lt;span class="si">}&lt;/span>.q mean_wrapper.sh &lt;span class="si">${&lt;/span>&lt;span class="nv">input_image&lt;/span>&lt;span class="si">}&lt;/span> &lt;span class="si">${&lt;/span>&lt;span class="nv">output_file&lt;/span>&lt;span class="si">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">done&lt;/span> &amp;lt;&lt;span class="si">${&lt;/span>&lt;span class="nv">subjects&lt;/span>&lt;span class="si">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Here&amp;rsquo;s an example output from running &lt;code>qstat&lt;/code> after submitting a batch of jobs to the cluster:&lt;/p>
&lt;figure id="figure-example-of-qstat-command-after-submitting-jobs-view-qsub">
&lt;a data-fancybox="" href="https://kristianeschenburg.netlify.app/post/submitting-batch-jobs-with-qsub/qsub_hu_f5f7230e53b0ace3.png" data-caption="Example of qstat command, after submitting jobs view qsub.">
&lt;img data-src="https://kristianeschenburg.netlify.app/post/submitting-batch-jobs-with-qsub/qsub_hu_f5f7230e53b0ace3.png" class="lazyload" alt="" width="799" height="323">
&lt;/a>
&lt;figcaption>
Example of qstat command, after submitting jobs view qsub.
&lt;/figcaption>
&lt;/figure>
&lt;p>One thing you&amp;rsquo;ll notice is the column &lt;code>priority&lt;/code> &amp;ndash; this is literally a &lt;code>priority queue&lt;/code> data structure that I mentioned in my last post on the Watershed by Flooding algorithm. Each job is submitted to the queue with a priority value assigned to it by the SGE software, and the jobs are processed in that order &amp;ndash; highest priority first, lowest priority last. Your IT manager can personalize the priority values for specific users or types of jobs, such that they are given preference or moved back in line. This represents an equitable way of distributing compute resources across users in a lab, generally using a first-come, first-serve basis, or restricting users to a certain number of nodes.&lt;/p></description></item></channel></rss>