Misharov Prohttps://blog.misharov.pro/2023-02-04T00:00:00+01:00How I backported Podman 4 to Ubuntu 22.042023-02-04T00:00:00+01:002023-02-04T00:00:00+01:00tag:blog.misharov.pro,2023-02-04:/2023-02-04/how-i-backported-podmanI love containers, it's really cool technology to preserve and distribute environments. To manage
containers I prefer podman and its friends buildah and skopeo. podman 4 introduced many cool
features but it's unavailable in Ubuntu 22.04 which is my primary OS, the latest version there is
3.4.4. Thus, I decided to backport podman from Debian Testing where it's already packaged by
mighty Debian maintainers.<p>I love containers, it's really cool technology to preserve and distribute environments. To manage
containers I prefer <code>podman</code> and its friends <code>buildah</code> and <code>skopeo</code>. <code>podman</code> 4 introduced many cool
features but it's unavailable in Ubuntu 22.04 which is my primary OS, the latest version there is
3.4.4. Thus, I decided to backport <code>podman</code> from Debian Testing where it's already packaged by
mighty Debian maintainers.</p>
<h2>Workflow</h2>
<p>As you might know Canonical drives Launchpad. This is a large portal that combines plenty of tools
around Ubuntu. One of these tools is PPA or Personal Package Archive. It allows you to build and
distribute your custom <code>deb</code> packages. I used many PPAs in the past but never created my own.
Moreover, I've never tried to build a <code>deb</code> package. There was a lot of fun ahead :)</p>
<p>To build <code>deb</code> packages Launchpad accepts so-called source packages as input. Fortunately, I didn't
have to make them from the scratch thanks to the great job of Debian maintainers. Thus, the
backporting process looked as follows:</p>
<ol>
<li>Get sources from Debian testing.</li>
<li>Update the changelog.</li>
<li>Rebuild the source package in Ubuntu 22.04.</li>
<li>Upload the resulting artifacts to my PPA.</li>
<li>Profit!</li>
</ol>
<h2>Fun</h2>
<p>First of all, it was hard to get the right documentation about package building in Debian. Don't get
me wrong, there is documentation but it's all over the place, it's duplicated and out of date
sometimes. Here are some links I found on <code>debian.org</code>:</p>
<ul>
<li><a href="https://wiki.debian.org/BuildingAPackage">https://wiki.debian.org/BuildingAPackage</a></li>
<li><a href="https://wiki.debian.org/Packaging">https://wiki.debian.org/Packaging</a></li>
<li><a href="https://wiki.debian.org/Packaging/Intro">https://wiki.debian.org/Packaging/Intro</a></li>
<li><a href="https://wiki.debian.org/HowToPackageForDebian">https://wiki.debian.org/HowToPackageForDebian</a></li>
<li><a href="https://wiki.debian.org/BuildingTutorial">https://wiki.debian.org/BuildingTutorial</a></li>
<li><a href="https://www.debian.org/doc/manuals/maint-guide/build.en.html">https://www.debian.org/doc/manuals/maint-guide/build.en.html</a></li>
</ul>
<p>After reading I got the understanding of what tools I should use and what commands need to be run.</p>
<h3>Tooling</h3>
<p>All package manipulations I did in the containers. I ran both <code>docker.io/library/ubuntu:22.04</code> and
<code>docker.io/library/debian:bookworm</code> with one shared volume:</p>
<div class="highlight"><pre><span></span><code>podman<span class="w"> </span>run<span class="w"> </span>-it<span class="w"> </span>--rm<span class="w"> </span>-v<span class="w"> </span><span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span>:/mnt<span class="w"> </span>docker.io/library/debian:bookworm<span class="w"> </span>bash
podman<span class="w"> </span>run<span class="w"> </span>-it<span class="w"> </span>--rm<span class="w"> </span>-v<span class="w"> </span><span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span>:/mnt<span class="w"> </span>docker.io/library/ubuntu:22.04<span class="w"> </span>bash
</code></pre></div>
<p>To build packages you need to install two packages: <code>devscripts</code> and <code>build-essential</code>. In the
Debian container <code>devscripts</code> will be enough. Don't forget to add source repositories into
<code>/etc/apt/sources.list</code> and update packages index:</p>
<div class="highlight"><pre><span></span><code><span class="nb">echo</span><span class="w"> </span><span class="s2">"deb-src http://debian.org/debian testing main"</span><span class="w"> </span>>><span class="w"> </span>/etc/apt/sources.list
apt-get<span class="w"> </span>update
apt-get<span class="w"> </span>install<span class="w"> </span>-y<span class="w"> </span>devscripts
</code></pre></div>
<h3>Sources</h3>
<p>To get the sources you should run only one command in the Debian container:</p>
<div class="highlight"><pre><span></span><code>apt-get<span class="w"> </span><span class="nb">source</span><span class="w"> </span>podman
</code></pre></div>
<p>There will be downloaded three files:</p>
<ul>
<li>libpod_4.3.1+ds1-5.dsc - package metadata</li>
<li>libpod_4.3.1+ds1-5.debian.tar.xz - an archive with building recipes, patches, package changelog
and other stuff</li>
<li>libpod_4.3.1+ds1.orig.tar.xz - source code</li>
</ul>
<p><code>dpkg-source</code> script from <code>devscripts</code> extracts archives applies patches and you will get a source
code directory with the <code>debian</code> directory within. Basically, that's the only action we should do in
the Debian container. The following operations I made in the Ubuntu container.</p>
<h3>Change log</h3>
<p>Before uploading sources to Launchpad a new entry in the <code>changelog</code> file should be added. It can be
done with any text editor but there is a dedicated utility <code>dch</code> that adds a time stamp and bumps
version. You just need to write what has been changed. I didn't change anything so I put only this
line:</p>
<div class="highlight"><pre><span></span><code>upload to ppa:quarckster/containers
</code></pre></div>
<h3>Building</h3>
<p>Here starts the <strong>fun</strong>. Launchpad accepts only source packages and to produce such you should run
<code>dpkg-buildpackage</code> from <code>devscripts</code>:</p>
<div class="highlight"><pre><span></span><code>dpkg-buildpackage<span class="w"> </span>-us<span class="w"> </span>-uc<span class="w"> </span>-sa<span class="w"> </span>-S
</code></pre></div>
<p>The command will fail due to missing <strong>build</strong> dependencies. I decided to ignore them and added
<code>-d</code> flag. After that I got four new files:</p>
<ul>
<li>libpod_4.3.1+ds1-5.1ubuntu1ppa1.debian.tar.xz</li>
<li>libpod_4.3.1+ds1-5.1ubuntu1ppa1.dsc</li>
<li>libpod_4.3.1+ds1-5.1ubuntu1ppa1_source.buildinfo</li>
<li>libpod_4.3.1+ds1-5.1ubuntu1ppa1_source.changes</li>
</ul>
<h3>Uploading to Launchpad</h3>
<p>Debian and Ubuntu provide utility named <code>dput</code> to upload source packages. All you need to do is just
to specify a path to <code>.changes</code> files and the PPA name:</p>
<div class="highlight"><pre><span></span><code>dput<span class="w"> </span>ppa:quarckster/containers<span class="w"> </span>libpod_4.3.1+ds1-5.1ubuntu1ppa1_source.changes
</code></pre></div>
<p>After that, all required files will be uploaded. But before you must sign <code>.changes</code> with your gpg
key:</p>
<div class="highlight"><pre><span></span><code>debsign<span class="w"> </span>-k<span class="w"> </span><span class="s2">"<key id>"</span><span class="w"> </span>libpod_4.3.1+ds1-5.1ubuntu1ppa1_source.changes
</code></pre></div>
<p>Actually, it can be done during the building but I didn't want to configure gpg in the container.</p>
<h3>Dependencies</h3>
<p>Do you remember I ignored build dependencies when I built the source package? Of course, my build
failed and I had to repack and upload the whole dependencies graph. Maybe there is a smart way to do
that but I didn't figure out anything better than waiting until a build fails, then check the logs,
finding missing dependencies and doing a new packaging iteration. I had to upload 38 packages in
total.</p>
<h3>Profit</h3>
<p>Now I have modern version of <code>podman</code> on my operating system. Feel free to use it as well.</p>
<p><a href="https://launchpad.net/~quarckster/+archive/ubuntu/containers">https://launchpad.net/~quarckster/+archive/ubuntu/containers</a></p>
<h2>Even more fun</h2>
<p>I felt a superpower and decided to backport a new networking backend for <code>podman</code>. It consists of
two packages <code>netavark</code> and <code>aardvark-dns</code>. I didn't realize how deep this rabbit hole. <code>podman</code> is
missing only 38 packages because most other dependencies from this graph are available in Ubuntu
22.04:</p>
<p><img alt="Podman dependency graph" src="https://blog.misharov.pro/assets/img/2023-02-06-podman_resized.png"></p>
<p><center>Fig. 1 - Podman dependency graph (<a href="/assets/img/2023-02-06-podman.png">high resolution</a>)</center></p>
<p>On the other hand, <code>netavark</code> and <code>aardvark-dns</code> are completely new packages and all build
dependencies exist only in Debian Testing:</p>
<p><img alt="Netavark dependency graph" src="https://blog.misharov.pro/assets/img/2023-02-06-netavark_resized.png"></p>
<p><center>Fig. 2 - Netavark dependency graph (<a href="/assets/img/2023-02-06-netavark.png">high resolution</a>)</center></p>
<p>When I uploaded around one hundred packages to my PPA I encountered circular dependency and further
backporting stuck.</p>
<h3>Vendoring</h3>
<p>Rust package manager <code>cargo</code> has ability to download all dependencies and configure the compiler
to use this local cache for building. You just need two commands:</p>
<div class="highlight"><pre><span></span><code>cargo<span class="w"> </span>vendor<span class="w"> </span>><span class="w"> </span>.cargo/config.toml
cargo<span class="w"> </span>generate-lockfile
</code></pre></div>
<p>New files should be included into <code>orig.tar.xz</code> archive and after that the package was successfully
built.</p>
<h2>Conclusion</h2>
<p>Packaging is fun :) I want to give credits to Debian maintainers because they do a great job. All I
need to do is just to change a couple of line in the <code>changelog</code> file and run some commands. The
most interesting part is located in the <code>rules</code> file which is a <code>Makefile</code>. It contains the recipe
and all logic.</p>
<blockquote>
<p>The only thing that really worried me was the <code>rules</code> file. There is nothing in the world more
helpless and irresponsible and depraved than a man in the depths of hacking <code>rules</code> file. And I
knew I'd get into that rotten stuff pretty soon. Probably next time.</p>
</blockquote>
<h2>References</h2>
<ul>
<li><a href="https://launchpad.net/~quarckster/+archive/ubuntu/containers">https://launchpad.net/~quarckster/+archive/ubuntu/containers</a></li>
<li><a href="https://help.launchpad.net/Packaging/PPA/Uploading">https://help.launchpad.net/Packaging/PPA/Uploading</a></li>
<li><a href="https://podman.io">https://podman.io</a></li>
<li><a href="https://github.com/containers/netavark">https://github.com/containers/netavark</a></li>
<li><a href="https://wiki.debian.org/BuildingAPackage">https://wiki.debian.org/BuildingAPackage</a></li>
<li><a href="https://wiki.debian.org/Packaging">https://wiki.debian.org/Packaging</a></li>
<li><a href="https://wiki.debian.org/Packaging/Intro">https://wiki.debian.org/Packaging/Intro</a></li>
<li><a href="https://wiki.debian.org/HowToPackageForDebian">https://wiki.debian.org/HowToPackageForDebian</a></li>
<li><a href="https://wiki.debian.org/BuildingTutorial">https://wiki.debian.org/BuildingTutorial</a></li>
<li><a href="https://www.debian.org/doc/manuals/maint-guide/build.en.html">https://www.debian.org/doc/manuals/maint-guide/build.en.html</a></li>
<li><a href="https://wiki.debian.org/DependencyHell">https://wiki.debian.org/DependencyHell</a></li>
</ul>
<p><a href="https://github.com/quarckster/blog.misharov.pro/discussions/28">Discuss on Github</a></p>Home-made remote video rendering2022-09-29T00:00:00+02:002022-09-29T00:00:00+02:00tag:blog.misharov.pro,2022-09-29:/2022-09-29/remote-video-renderingRecently I started my video blog on YouTube. To shoot videos I use DJI Action 2 camcorder. It can
record videos up to 4K@60 FPS. Obviously, resulting video files are heavy for editing and rendering.
Are there solutions on Linux that help you to work with modern high definition video? The answer is
yes and here is my approach.<p>Recently I started my video blog on YouTube. To shoot videos I use DJI Action 2 camcorder. It can
record videos up to 4K@60 FPS. Obviously, resulting video files are heavy for editing and rendering.
Are there solutions on Linux that help you to work with modern high definition video? The answer is
yes and here is my approach.</p>
<h2>Editing</h2>
<p>To edit video I prefer <code>Kdenlive</code>. It's a powerful open-source video editing software. The UI part
is powered by KDE and QT technologies. Rendering and editing is based on <code>melt</code> framework.
<code>Kdenlive</code> has interesting feature named <code>proxy clips</code>. Even the high-end CPUs will be brought down
to the knees when you try to edit 4K video in real time. So instead of working with original files
<code>Kdenlive</code> uses files with lower quality. My camcorder records these low-res files along with
original ones. This drastically improves your editing experience. When you render your project the
original files will be used.</p>
<h2>Rendering</h2>
<p>Looks like you cannot cheat physics and you have to wait for a long time until the project will be
rendered on a weak laptop CPU. But it's not true :) What if the rendering could be performed
remotely on a more powerful machine? You just need an instance on some cloud provider such as AWS,
GCP or Azure. <code>Kdenlive</code> can generate a script for <code>melt</code> command line utility. So you don't need to
install <code>Kdenlive</code> on a remote machine. And what about video files? I the beginning I decided to
upload them using <code>rsync</code> but it would take several hours for my project. I had several hundreds of
gigabytes. I started to think if it's possible <strong>to mount</strong> my local directory on the remote
machine. Fortunately, everything can be done with a couple of commands. You just need <code>ssh</code> and
<code>sshfs</code>. Here are commands for Ubuntu:</p>
<ol>
<li>
<p>Install the ssh server.</p>
</li>
<li>
<p>Run an <code>sftp</code> server on some port on the <strong>local</strong> machine:</p>
</li>
</ol>
<div class="highlight"><pre><span></span><code><span class="nb">cd</span><span class="w"> </span><PATH<span class="w"> </span>THAT<span class="w"> </span>SHOULD<span class="w"> </span>BE<span class="w"> </span>MOUNTED<span class="w"> </span>ON<span class="w"> </span>THE<span class="w"> </span>REMOTE<span class="w"> </span>MACHINE>
ncat<span class="w"> </span>-l<span class="w"> </span>-p<span class="w"> </span><span class="m">34567</span><span class="w"> </span>-e<span class="w"> </span>/usr/lib/openssh/sftp-server<span class="w"> </span><span class="p">&</span>
</code></pre></div>
<ol>
<li>Make a tunnel from the remote instance to the port on the local machine and run <code>sshfs</code> and
<code>bash</code>. Make sure that path exists on the remote host:</li>
</ol>
<div class="highlight"><pre><span></span><code>ssh<span class="w"> </span>-t<span class="w"> </span>-R<span class="w"> </span><span class="m">34568</span>:localhost:34567<span class="w"> </span><USER@REMOTE<span class="w"> </span>HOST><span class="w"> </span><span class="s2">"sshfs localhost: <LOCAL MOUNT POINT> -o directport=34568; bash"</span>
</code></pre></div>
<p>After that all you need is to run <code>melt <PATH TO SCRIPT></code>. One thing you should know that the script
contains absolute paths to the video files. So you should either edit the script or use the same
paths on both local and remote machines.</p>
<h2>Results</h2>
<p>Obviously you should have the fast enough internet connection otherwise it will be the bottleneck of
the whole pipeline. I tried that trick with two Digital Ocean instances: 48 CPUs and 16 CPUs. I
noticed that the more powerful instance doesn't give you 3x boost performance. Looks like <code>ffmpeg</code>
and encoders such as <code>x265</code> don't utilize all available CPUs. Anyway, using this approach with fast
internet connection you will speed up video rendering.</p>
<p>References:</p>
<ul>
<li><a href="https://mltframework.org">https://mltframework.org</a></li>
<li><a href="https://kdenlive.org">https://kdenlive.org</a></li>
</ul>
<p><a href="https://github.com/quarckster/blog.misharov.pro/discussions/25">Discuss on Github</a></p>Debugging system python scripts2022-08-30T00:00:00+02:002022-08-30T00:00:00+02:00tag:blog.misharov.pro,2022-08-30:/2022-08-30/debug-system-python-scriptsI faced with broken add-apt-repository on my KDE Neon after upgrading to Ubuntu 22.04<p>I faced with broken <code>add-apt-repository</code> on my KDE Neon after upgrading to Ubuntu 22.04</p>
<div class="highlight"><pre><span></span><code>aptsources.distro.NoDistroTemplateException: Error: could not find a distribution template for Neon/jammy
</code></pre></div>
<p>Though I knew that the issue was in <code>/etc/os-release</code> file I wanted to debug <code>add-apt-repository</code>
using <a href="https://blog.misharov.pro/2020-05-24/best-python-debugger">my favorite debugger</a>.</p>
<h2>Naive approach</h2>
<p>It's a python script that comes with Ubuntu to manage repositories from PPA. If it's a python script
it can be debugged with <code>pudb</code>. But first of all we need to install it:</p>
<ol>
<li>
<p>Create a fresh virtual environment:</p>
<div class="highlight"><pre><span></span><code>virtualenv ~/venvs/pudb
Created virtual environment CPython3.10.4.final.0-64 in 216ms
creator CPython3Posix(dest=/home/misharov/venvs/pudb, clear=False, no_vcs_ignore=False, global=False)
seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=/home/misharov/.local/share/virtualenv)
added seed packages: pip==22.0.2, setuptools==59.6.0, wheel==0.37.1
activators BashActivator,CShellActivator,FishActivator,NushellActivator,PowerShellActivator,PythonActivator
</code></pre></div>
</li>
<li>
<p>Activate it:</p>
<div class="highlight"><pre><span></span><code>source /home/misharov/venvs/pudb/bin/activate
</code></pre></div>
</li>
<li>
<p>Install <code>pudb</code>:</p>
<div class="highlight"><pre><span></span><code>pip install pudb
</code></pre></div>
</li>
<li>
<p>Run <code>add-apt-repository</code> in the debug mode. The naive approach would be running the script like
this:</p>
<div class="highlight"><pre><span></span><code>python -m pudb /usr/bin/add-apt-repository
</code></pre></div>
</li>
</ol>
<p>It will be executed until you will get the following error:</p>
<div class="highlight"><pre><span></span><code>ModuleNotFoundError: No module named 'apt_pkg'
</code></pre></div>
<p>Ouch, the naive approach was wrong. Our virtual environment is not aware about system installed
packages. In Ubuntu 22.04 system python packages are installed in the following paths:</p>
<div class="highlight"><pre><span></span><code>python3<span class="w"> </span>-c<span class="w"> </span><span class="s1">'import site; print(site.getsitepackages())'</span>
<span class="o">[</span><span class="s1">'/usr/local/lib/python3.10/dist-packages'</span>,<span class="w"> </span><span class="s1">'/usr/lib/python3/dist-packages'</span>,<span class="w"> </span><span class="s1">'/usr/lib/python3.10/dist-packages'</span><span class="o">]</span>
</code></pre></div>
<p><code>apt_pkg</code> can be found in <code>/usr/lib/python3/dist-packages</code></p>
<h2>The right way</h2>
<p>Someone would propose to install <code>pudb</code> along with system python packages but it might even break
the system python. Therefore never run <code>sudo pip install</code>! There is a better way. <code>virtualenv</code>
can create a virtual environment that uses system packages:</p>
<div class="highlight"><pre><span></span><code>virtualenv<span class="w"> </span>--system-site-packages<span class="w"> </span>~/venvs/pudb
created<span class="w"> </span>virtual<span class="w"> </span>environment<span class="w"> </span>CPython3.10.4.final.0-64<span class="w"> </span><span class="k">in</span><span class="w"> </span>114ms
<span class="w"> </span>creator<span class="w"> </span>CPython3Posix<span class="o">(</span><span class="nv">dest</span><span class="o">=</span>/home/misharov/venvs/pudb,<span class="w"> </span><span class="nv">clear</span><span class="o">=</span>False,<span class="w"> </span><span class="nv">no_vcs_ignore</span><span class="o">=</span>False,<span class="w"> </span><span class="nv">global</span><span class="o">=</span>True<span class="o">)</span>
<span class="w"> </span>seeder<span class="w"> </span>FromAppData<span class="o">(</span><span class="nv">download</span><span class="o">=</span>False,<span class="w"> </span><span class="nv">pip</span><span class="o">=</span>bundle,<span class="w"> </span><span class="nv">setuptools</span><span class="o">=</span>bundle,<span class="w"> </span><span class="nv">wheel</span><span class="o">=</span>bundle,<span class="w"> </span><span class="nv">via</span><span class="o">=</span>copy,<span class="w"> </span><span class="nv">app_data_dir</span><span class="o">=</span>/home/misharov/.local/share/virtualenv<span class="o">)</span>
<span class="w"> </span>added<span class="w"> </span>seed<span class="w"> </span>packages:<span class="w"> </span><span class="nv">pip</span><span class="o">==</span><span class="m">22</span>.0.2,<span class="w"> </span><span class="nv">setuptools</span><span class="o">==</span><span class="m">59</span>.6.0,<span class="w"> </span><span class="nv">wheel</span><span class="o">==</span><span class="m">0</span>.37.1
<span class="w"> </span>activators<span class="w"> </span>BashActivator,CShellActivator,FishActivator,NushellActivator,PowerShellActivator,PythonActivator
</code></pre></div>
<p>New packages will be installed into the virtual environment and system packages will be accounted
as well. After activation and installation <code>pudb</code> we can run <code>add-apt-repository</code> in the debug mode.</p>
<p><a href="https://github.com/quarckster/blog.misharov.pro/discussions/25">Discuss on Github</a></p>Zsh in Debian2022-07-15T00:00:00+02:002022-07-15T00:00:00+02:00tag:blog.misharov.pro,2022-07-15:/2022-07-15/zsh-in-debianAfter installing some Flatpak package on my KDE Neon the application icon was not displaying neither
in the window header nor in the panel. I wanted to fix it and discovered that is related to zsh
package. Here is my troubleshooting path.<p>After installing some Flatpak package on my KDE Neon the application icon was not displaying neither
in the window header nor in the panel. I wanted to fix it and discovered that is related to <code>zsh</code>
package. Here is my troubleshooting path.</p>
<h1>Flatpak</h1>
<p>Let's start with <code>Flatpak</code>. It's a way to distribute applications in Linux. It has some pros and
cons but I prefer it to install some proprietary apps and appps that are not supplied in the
distro repositories. After installing Telegram from Flatpak I noticed that the application icon
is missing in the task panel and in the window header. I started to search how Flatpak propagates
information about the app.</p>
<h1>Freedesktop</h1>
<p>First of all such application resources as icons are stored in <code>share</code> directories. In Debian and
Ubuntu it's <code>/usr/local/share</code> and <code>/usr/share</code>. And there is an environments variable
<code>XDG_DATA_DIRS</code> which is a part of Freedesktop specification. Flatpak has its own directories for
application resources <code>$HOME/.local/share/flatpak/exports/share</code> and
<code>/var/lib/flatpak/exports/share</code>. And obviously it should add these directories into
<code>XDG_DATA_DIRS</code>. But they were not there.</p>
<h1>Again Flatpak</h1>
<p>So how Flatpak updates <code>XDG_DATA_DIRS</code>. Arch wiki has a mention of it:</p>
<blockquote>
<p>Flatpak expects window managers to respect the XDG_DATA_DIRS environment variable to discover
applications. This variable is set by the script /etc/profile.d/flatpak.sh. Updating the
environment may require restarting the session.</p>
</blockquote>
<p>This is it. <code>flatpak.sh</code> is executed from your <code>/etc/profile.d/</code> directory. But wait. This directory
is used by <code>/etc/profile</code> script. Basically it's the very first script that is executed by <code>bash</code>
when it's invoked.</p>
<h1>Zsh</h1>
<p>But I use <code>zsh</code> not <code>bash</code>. I even use it as a login shell. Here is an excerpt from my
<code>/etc/passwd</code>:</p>
<div class="highlight"><pre><span></span><code>dmisharo:x:1000:1000:Dmitry Misharov:/home/dmisharo:/usr/bin/zsh
</code></pre></div>
<p><code>Zsh</code> doesn't source <code>/etc/profile</code>, it sources <code>zprofile</code> instead. And here is the most interesting
part. Unlike other popular Linux distributions Debian (and Ubuntu) supplies empty
<code>/etc/zsh/zprofile</code>:</p>
<div class="highlight"><pre><span></span><code><span class="c1"># /etc/zsh/zprofile: system-wide .zprofile file for zsh(1).</span>
<span class="c1">#</span>
<span class="c1"># This file is sourced only for login shells (i.e. shells</span>
<span class="c1"># invoked with "-" as the first character of argv[0], and</span>
<span class="c1"># shells invoked with the -l flag.)</span>
<span class="c1">#</span>
<span class="c1"># Global Order: zshenv, zprofile, zshrc, zlogin</span>
</code></pre></div>
<p>Fedora 36:</p>
<div class="highlight"><pre><span></span><code><span class="c1">#</span>
<span class="c1"># /etc/zprofile and ~/.zprofile are run for login shells</span>
<span class="c1">#</span>
_src_etc_profile<span class="o">()</span>
<span class="o">{</span>
<span class="w"> </span><span class="c1"># Make /etc/profile happier, and have possible ~/.zshenv options like</span>
<span class="w"> </span><span class="c1"># NOMATCH ignored.</span>
<span class="w"> </span><span class="c1">#</span>
<span class="w"> </span>emulate<span class="w"> </span>-L<span class="w"> </span>ksh
<span class="w"> </span><span class="c1"># source profile</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="o">[</span><span class="w"> </span>-f<span class="w"> </span>/etc/profile<span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w"> </span><span class="nb">source</span><span class="w"> </span>/etc/profile
<span class="w"> </span><span class="k">fi</span>
<span class="o">}</span>
_src_etc_profile
<span class="nb">unset</span><span class="w"> </span>-f<span class="w"> </span>_src_etc_profile
</code></pre></div>
<p>Arch:</p>
<div class="highlight"><pre><span></span><code>emulate<span class="w"> </span>sh<span class="w"> </span>-c<span class="w"> </span><span class="s1">'source /etc/profile'</span>
</code></pre></div>
<p>Debian developers intentionally do not source <code>/etc/profile</code> in <code>zprofile</code>:</p>
<blockquote>
<p>We need to keep so-called login-shell always as Bash if you don't want to drive
yourself crazy. Adding workarounds for all possible choices of so-called POSIX shell
is just waste of resource. If we make work around for zsh, then we need to do it for
ksh, posh, .... POSIX is no magic word for shells to be treated as equal since there
are subtle differences.</p>
</blockquote>
<h1>My solution</h1>
<p>I ended up just copying Arch way and my <code>zprofile</code> sources <code>/etc/profile</code> via emulation.</p>
<h1>References:</h1>
<ul>
<li><a href="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html</a></li>
<li><a href="https://wiki.archlinux.org/title/Flatpak#Add_Flatpak_.desktop_files_to_your_menu">https://wiki.archlinux.org/title/Flatpak#Add_Flatpak_.desktop_files_to_your_menu</a></li>
<li><a href="https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=983116#22">https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=983116#22</a></li>
</ul>
<p><a href="https://github.com/quarckster/blog.misharov.pro/discussions/24">Discuss on Github</a></p>Virtual machine image customization2021-12-11T00:00:00+01:002021-12-11T00:00:00+01:00tag:blog.misharov.pro,2021-12-11:/2021-12-11/virt-customizeI work with Linux containers and container images everyday. I often create new images using podman
or buildah. One day I needed to build a custom image of a virtual machine and in this post you'll
read how I did it.<p>I work with Linux containers and container images everyday. I often create new images using <code>podman</code>
or <code>buildah</code>. One day I needed to build a custom image of a virtual machine and in this post you'll
read how I did it.</p>
<h2>Prerequisite</h2>
<p>In the post <a href="https://blog.misharov.pro/2021-06-18/gitlab-openstack-executor">GitLab custom executor for Openstack</a>
I explained how to create a GitLab executor for Openstack. That executor provisions an Openstack
instance for each job and run a job script in it. There is some required software to run jobs:</p>
<blockquote>
<p>The user must set up the environment, including the following that must be present in the <code>PATH</code>:</p>
<p>Git: Used to clone the repositories.</p>
<p>Git LFS: Pulls any LFS objects that might be in the repository.</p>
<p>GitLab Runner: Used to download/update artifacts and cache. </p>
</blockquote>
<p>It means that we need to customize an instance image to include prerequisites.</p>
<h2>libguestfs</h2>
<p>After some research I found a tool that does exactly what I need. <code>libguestfs</code> project provides
a set of utilities for accessing and modifying virtual machine disk images. <code>virt-customize</code> can
customize disk images in place. I chose Fedora Cloud image as environment for running GitLab jobs.
The commands to add required software are very simple:</p>
<ol>
<li>Install <code>libguestfs</code></li>
<li>Download or build <code>gitlab-runner</code></li>
<li>
<p>Create a file with a <code>virt-customize</code> scenario:</p>
<p><code>sh
cat > commands.txt <<EOF
install git-core,git-lfs
copy-in <path to gitlab-runner binary>:/usr/bin/
chmod 0755:/usr/bin/gitlab-runner
selinux-relabel
EOF</code></p>
</li>
<li>
<p>Run <code>virt-customize</code>:</p>
<p><code>sh
virt-customize -v -a Fedora-Cloud-Base-35-1.2.x86_64.qcow2 --commands-from-file commands.txt</code></p>
</li>
</ol>
<p>Pay attention to the <code>selinux-relabel</code> customization option. This is required for VMs that supports
SELinux. Fedora instance cannot properly start if you modified the image without relabelling.</p>
<p>References:</p>
<ul>
<li><a href="https://libguestfs.org/virt-customize.1.html">https://libguestfs.org/virt-customize.1.html</a></li>
<li><a href="https://docs.gitlab.com/runner/executors/custom.html#prerequisite-software-for-running-a-job">https://docs.gitlab.com/runner/executors/custom.html#prerequisite-software-for-running-a-job</a></li>
<li><a href="https://alt.fedoraproject.org/cloud/">https://alt.fedoraproject.org/cloud/</a></li>
</ul>
<p><a href="https://github.com/quarckster/blog.misharov.pro/discussions/23">Discuss on Github</a></p>Minimalistic Selenium container image2021-10-02T00:00:00+02:002021-10-02T00:00:00+02:00tag:blog.misharov.pro,2021-10-02:/2021-10-02/selenium-container-imageContainers are de facto a standard way to distribute services and software. It's convenient
to have preconfigured environment that can be activated on any machine that has a container engine
such as docker or podman. UI testing with Selenium requires certain environment including
browsers, X server and a window manager. It would be nice to have all of these packed into a one
container image.<p>Containers are de facto a standard way to distribute services and software. It's convenient
to have preconfigured environment that can be activated on any machine that has a container engine
such as <code>docker</code> or <code>podman</code>. UI testing with Selenium requires certain environment including
browsers, X server and a window manager. It would be nice to have all of these packed into a one
container image.</p>
<h2>SeleniumHQ official container image</h2>
<p>Selenium developers offer their container images on <a href="https://github.com/SeleniumHQ/docker-selenium">https://github.com/SeleniumHQ/docker-selenium</a>.
There are several "flavors" for different purposes and with various content. Despite these images
are almost standard for UI testing I find them a bit bloated. If you run the following command:</p>
<div class="highlight"><pre><span></span><code>podman<span class="w"> </span>run<span class="w"> </span>-it<span class="w"> </span>--rm<span class="w"> </span>selenium/standalone-firefox:latest<span class="w"> </span>dpkg<span class="w"> </span>-l
</code></pre></div>
<p>you will see such packages as <code>systemd</code> and <code>dbus</code> that are installed by a package manager but they
are not required for running Selenium, X server, and browsers. Moreover, these images were not
optimized to run in Openshift clusters where containers are executed under an arbitrary user id by
the moment we started to perform UI tests there. Therefore, we at Red Hat QE maintain our Selenium
container image.</p>
<h2>Red Hat QE Selenium container image</h2>
<h3>Defining prerequisites</h3>
<p>First of all, we based our container image on Fedora and second we install both Firefox and Google
Chrome. Originally, package dependencies were fully resolved by <code>dnf</code> package manager. It behaves
the same as it was on the usual desktop and installs a lot of packages that are not needed for UI
testing. I wanted to try to find the minimal set of rpm packages for the main components of the UI
testing with Selenium, Firefox, and Google Chrome browsers. We run our tests in headful mode and for
that, we need X server. To manage a browser window a window manager is required. <code>fluxbox</code> is a good
choice and I agree with Selenium developers on that. To inspect what is going on during the testing
session we can use a VNC server. Here enters <code>tigervnc</code>. It has a killer feature - <code>Xvnc</code> server:</p>
<blockquote>
<p>It is based on a standard X server, but it has a "virtual" screen rather than a physical one. X
applications display themselves on it as if it were a normal X display, but they can only be
accessed via a VNC viewer.</p>
</blockquote>
<p>Thus, <code>tigervnc</code> provides both X and VNC servers. The final list of the prerequisites in the
installation order:</p>
<ul>
<li>X and VNC servers</li>
<li>Window manager</li>
<li>Browsers</li>
<li>Web drivers</li>
<li>Selenium standalone server</li>
</ul>
<h3>Finding the minimal set of packages</h3>
<p>I spent an evening doing some manual operations to find only the required packages for software from
our prerequisites list. The algorithm is simple:</p>
<ol>
<li>
<p>Start a container:</p>
<div class="highlight"><pre><span></span><code>podman run -it --rm registry.fedoraproject.org/fedora:34 bash
</code></pre></div>
</li>
<li>
<p>Download or install the prerequisite software without dependencies:</p>
<div class="highlight"><pre><span></span><code>dnf install -y tar bzip2 dnf-plugins-core
curl -LO https://download-installer.cdn.mozilla.net/pub/firefox/releases/91.1.0esr/linux-x86_64/en-US/firefox-91.1.0esr.tar.bz2
tar -C . -xjvf firefox-91.1.0esr.tar.bz2
</code></pre></div>
</li>
<li>
<p>Start the command:</p>
<div class="highlight"><pre><span></span><code>cd firefox
./firefox
XPCOMGlueLoad error for file /firefox/libmozgtk.so:
libgtk-3.so.0: cannot open shared object file: No such file or directory
Couldn't load XPCOM.
</code></pre></div>
</li>
<li>
<p>Find an rpm package that provides the required file:</p>
<div class="highlight"><pre><span></span><code>dnf whatprovides libgtk-3.so.0
...
gtk3-3.24.28-2.fc34.i686 : GTK+ graphical user interface library
Repo : fedora
Matched from:
Provide : libgtk-3.so.0
gtk3-3.24.30-1.fc34.i686 : GTK+ graphical user interface library
Repo : updates
Matched from:
Provide : libgtk-3.so.0
</code></pre></div>
</li>
<li>
<p>Download and install the package:</p>
<div class="highlight"><pre><span></span><code>dnf download --archlist=x86_64,noarch <package name>
rpm -Uvh --nodeps <package name>
</code></pre></div>
</li>
<li>
<p>Save the package name without name and architecture.</p>
</li>
<li>Repeat steps 3-6 until "cannot open shared object file" error will go.</li>
</ol>
<p>In the end, I've created a Dockerfile that can be used as a base for other images:</p>
<div class="highlight"><pre><span></span><code><span class="k">FROM</span><span class="w"> </span><span class="s">registry.fedoraproject.org/fedora-minimal:34</span>
<span class="k">LABEL</span><span class="w"> </span><span class="nv">maintainer</span><span class="o">=</span><span class="s2">"dmisharo@redhat.com"</span>
<span class="k">ENV</span><span class="w"> </span><span class="nv">SELENIUM_HOME</span><span class="o">=</span>/home/selenium
<span class="k">WORKDIR</span><span class="w"> </span><span class="s">${SELENIUM_HOME}</span>
<span class="k">RUN</span><span class="w"> </span><span class="nv">PACKAGES</span><span class="o">=</span><span class="s2">"\</span>
<span class="s2"> alsa-lib \</span>
<span class="s2"> at-spi2-atk \</span>
<span class="s2"> at-spi2-core \</span>
<span class="s2"> atk \</span>
<span class="s2"> avahi-libs \</span>
<span class="s2"> bzip2 \</span>
<span class="s2"> cairo \</span>
<span class="s2"> cairo-gobject \</span>
<span class="s2"> cups-libs \</span>
<span class="s2"> dbus-glib \</span>
<span class="s2"> dbus-libs \</span>
<span class="s2"> expat \</span>
<span class="s2"> fluxbox \</span>
<span class="s2"> fontconfig \</span>
<span class="s2"> freetype \</span>
<span class="s2"> fribidi \</span>
<span class="s2"> gdk-pixbuf2 \</span>
<span class="s2"> graphite2 \</span>
<span class="s2"> gtk3 \</span>
<span class="s2"> harfbuzz \</span>
<span class="s2"> imlib2 \</span>
<span class="s2"> java-1.8.0-openjdk-headless \</span>
<span class="s2"> libcloudproviders \</span>
<span class="s2"> libdatrie \</span>
<span class="s2"> libdrm \</span>
<span class="s2"> libepoxy \</span>
<span class="s2"> liberation-fonts \</span>
<span class="s2"> liberation-fonts-common \</span>
<span class="s2"> liberation-mono-fonts \</span>
<span class="s2"> liberation-sans-fonts \</span>
<span class="s2"> liberation-serif-fonts \</span>
<span class="s2"> libfontenc \</span>
<span class="s2"> libglvnd \</span>
<span class="s2"> libglvnd-glx \</span>
<span class="s2"> libICE \</span>
<span class="s2"> libjpeg-turbo \</span>
<span class="s2"> libpng \</span>
<span class="s2"> libSM \</span>
<span class="s2"> libthai \</span>
<span class="s2"> libwayland-client \</span>
<span class="s2"> libwayland-cursor \</span>
<span class="s2"> libwayland-egl \</span>
<span class="s2"> libwayland-server \</span>
<span class="s2"> libwebp \</span>
<span class="s2"> libX11 \</span>
<span class="s2"> libX11-common \</span>
<span class="s2"> libX11-xcb \</span>
<span class="s2"> libXau \</span>
<span class="s2"> libxcb \</span>
<span class="s2"> libXcomposite \</span>
<span class="s2"> libXcursor \</span>
<span class="s2"> libXdamage \</span>
<span class="s2"> libXdmcp \</span>
<span class="s2"> libXext \</span>
<span class="s2"> libXfixes \</span>
<span class="s2"> libXfont2 \</span>
<span class="s2"> libXft \</span>
<span class="s2"> libXi \</span>
<span class="s2"> libXinerama \</span>
<span class="s2"> libxkbcommon \</span>
<span class="s2"> libxkbfile \</span>
<span class="s2"> libXpm \</span>
<span class="s2"> libXrandr \</span>
<span class="s2"> libXrender \</span>
<span class="s2"> libxshmfence \</span>
<span class="s2"> libXt \</span>
<span class="s2"> mesa-libgbm \</span>
<span class="s2"> nspr \</span>
<span class="s2"> nss \</span>
<span class="s2"> nss-softokn \</span>
<span class="s2"> nss-softokn-freebl \</span>
<span class="s2"> nss-util \</span>
<span class="s2"> pango \</span>
<span class="s2"> pixman \</span>
<span class="s2"> tar \</span>
<span class="s2"> tigervnc-server-minimal \</span>
<span class="s2"> tzdata-java \</span>
<span class="s2"> unzip \</span>
<span class="s2"> vulkan-loader \</span>
<span class="s2"> wget \</span>
<span class="s2"> xdg-utils \</span>
<span class="s2"> xkbcomp \</span>
<span class="s2"> xkeyboard-config"</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>microdnf<span class="w"> </span>download<span class="w"> </span>-y<span class="w"> </span>--archlist<span class="o">=</span>x86_64,noarch<span class="w"> </span><span class="si">${</span><span class="nv">PACKAGES</span><span class="si">}</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>rpm<span class="w"> </span>-Uvh<span class="w"> </span>--nodeps<span class="w"> </span>*.rpm<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>rm<span class="w"> </span>-f<span class="w"> </span>*.rpm<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>microdnf<span class="w"> </span>clean<span class="w"> </span>all
</code></pre></div>
<p>As you can see I completely ignore dependencies by providing <code>--nodeps</code> argument to <code>rpm</code> command.
Besides, I used a minimal Fedora image to even more reduce the amount of preinstalled packages.</p>
<h2>Init</h2>
<p>The common practice is to run one process per container. If you need to make some IPC you do it via
networking protocols. In the case of the Selenium container image, all components have to be run in the same
container. We need three running processes: <code>selenium-server-standalone</code>, <code>fluxbox</code>
and <code>Xvnc</code>. Moreover, they should be started and finished in the right order:</p>
<p><code>Xvnc</code> -> <code>fluxbox</code> -> <code>selenium-server-standalone</code></p>
<p>We could use a simple shell script like this one:</p>
<div class="highlight"><pre><span></span><code><span class="ch">#!/bin/bash</span>
Xvnc<span class="w"> </span><span class="si">${</span><span class="nv">DISPLAY</span><span class="si">}</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-alwaysshared<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-depth<span class="w"> </span><span class="m">16</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-geometry<span class="w"> </span><span class="si">${</span><span class="nv">VNC_GEOMETRY</span><span class="si">}</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-securitytypes<span class="w"> </span>none<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-auth<span class="w"> </span><span class="si">${</span><span class="nv">HOME</span><span class="si">}</span>/.Xauthority<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-fp<span class="w"> </span>catalogue:/etc/X11/fontpath.d<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-pn<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-rfbport<span class="w"> </span><span class="m">59</span><span class="k">$(</span><span class="nb">echo</span><span class="w"> </span><span class="si">${</span><span class="nv">DISPLAY</span><span class="si">}</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>tr<span class="w"> </span>-d<span class="w"> </span><span class="s2">":"</span><span class="k">)</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-rfbwait<span class="w"> </span><span class="m">30000</span><span class="w"> </span>><span class="w"> </span>/dev/null<span class="w"> </span><span class="m">2</span>><span class="p">&</span><span class="m">1</span><span class="w"> </span><span class="p">&</span>
sleep<span class="w"> </span><span class="m">3</span>
fluxbox<span class="w"> </span>><span class="w"> </span>/dev/null<span class="w"> </span><span class="m">2</span>><span class="p">&</span><span class="m">1</span><span class="w"> </span><span class="p">&</span>
java<span class="w"> </span>-jar<span class="w"> </span><span class="si">${</span><span class="nv">SELENIUM_PATH</span><span class="si">}</span><span class="w"> </span>-port<span class="w"> </span><span class="si">${</span><span class="nv">SELENIUM_PORT</span><span class="si">}</span><span class="w"> </span><span class="m">2</span>><span class="p">&</span><span class="m">1</span>
</code></pre></div>
<p>It worked fine but after a while, I discovered that <code>fluxbox</code> core dumped if you stop the container.
It doesn't affect anything but it annoyed me. I tried to gracefully handle <code>SIGTERM</code> <code>SIGINT</code> in
the script:</p>
<div class="highlight"><pre><span></span><code><span class="nb">trap</span><span class="w"> </span>cleanup<span class="w"> </span>SIGTERM<span class="w"> </span>SIGINT
cleanup<span class="o">()</span><span class="w"> </span><span class="o">{</span>
<span class="w"> </span>pkill<span class="w"> </span>java<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>pkill<span class="w"> </span>fluxbox<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>pkill<span class="w"> </span>Xvnc
<span class="o">}</span>
</code></pre></div>
<p>But it didn't work for me. For some reason init script didn't stop the processes in the right order
and <code>fluxbox</code> continued causing core dump. Then I decided to write a simple init program in Go lang
that would do exactly what I want. Here it is:</p>
<div class="highlight"><pre><span></span><code><span class="kn">package</span><span class="w"> </span><span class="nx">main</span>
<span class="kn">import</span><span class="w"> </span><span class="p">(</span>
<span class="w"> </span><span class="s">"bufio"</span>
<span class="w"> </span><span class="s">"fmt"</span>
<span class="w"> </span><span class="s">"io"</span>
<span class="w"> </span><span class="s">"net"</span>
<span class="w"> </span><span class="s">"os"</span>
<span class="w"> </span><span class="s">"os/exec"</span>
<span class="w"> </span><span class="s">"os/signal"</span>
<span class="w"> </span><span class="s">"syscall"</span>
<span class="w"> </span><span class="s">"time"</span>
<span class="p">)</span>
<span class="kd">func</span><span class="w"> </span><span class="nx">startXvnc</span><span class="p">()</span><span class="w"> </span><span class="o">*</span><span class="nx">exec</span><span class="p">.</span><span class="nx">Cmd</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">xvnc</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">exec</span><span class="p">.</span><span class="nx">Command</span><span class="p">(</span>
<span class="w"> </span><span class="s">"Xvnc"</span><span class="p">,</span>
<span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nx">Getenv</span><span class="p">(</span><span class="s">"DISPLAY"</span><span class="p">),</span>
<span class="w"> </span><span class="s">"-alwaysshared"</span><span class="p">,</span>
<span class="w"> </span><span class="s">"-depth"</span><span class="p">,</span>
<span class="w"> </span><span class="s">"16"</span><span class="p">,</span>
<span class="w"> </span><span class="s">"-geometry"</span><span class="p">,</span>
<span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nx">Getenv</span><span class="p">(</span><span class="s">"VNC_GEOMETRY"</span><span class="p">),</span>
<span class="w"> </span><span class="s">"-securitytypes"</span><span class="p">,</span>
<span class="w"> </span><span class="s">"none"</span><span class="p">,</span>
<span class="w"> </span><span class="s">"-auth"</span><span class="p">,</span>
<span class="w"> </span><span class="nx">fmt</span><span class="p">.</span><span class="nx">Sprintf</span><span class="p">(</span><span class="s">"%s/.Xauthority"</span><span class="p">,</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nx">Getenv</span><span class="p">(</span><span class="s">"HOME"</span><span class="p">)),</span>
<span class="w"> </span><span class="s">"-fp"</span><span class="p">,</span>
<span class="w"> </span><span class="s">"catalogue:/etc/X11/fontpath.d"</span><span class="p">,</span>
<span class="w"> </span><span class="s">"-pn"</span><span class="p">,</span>
<span class="w"> </span><span class="s">"-rfbport"</span><span class="p">,</span>
<span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nx">Getenv</span><span class="p">(</span><span class="s">"VNC_PORT"</span><span class="p">),</span>
<span class="w"> </span><span class="s">"-rfbwait"</span><span class="p">,</span>
<span class="w"> </span><span class="s">"30000"</span><span class="p">)</span>
<span class="w"> </span><span class="nx">fmt</span><span class="p">.</span><span class="nx">Println</span><span class="p">(</span><span class="s">"Starting Xvnc"</span><span class="p">)</span>
<span class="w"> </span><span class="nx">xvnc</span><span class="p">.</span><span class="nx">Start</span><span class="p">()</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">xvnc</span>
<span class="p">}</span>
<span class="kd">func</span><span class="w"> </span><span class="nx">waitForPort</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">n</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">1</span>
<span class="w"> </span><span class="nx">address</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">net</span><span class="p">.</span><span class="nx">JoinHostPort</span><span class="p">(</span><span class="s">"localhost"</span><span class="p">,</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nx">Getenv</span><span class="p">(</span><span class="s">"VNC_PORT"</span><span class="p">))</span>
<span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="nx">n</span><span class="w"> </span><span class="p"><</span><span class="w"> </span><span class="mi">50</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">conn</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">net</span><span class="p">.</span><span class="nx">Dial</span><span class="p">(</span><span class="s">"tcp"</span><span class="p">,</span><span class="w"> </span><span class="nx">address</span><span class="p">)</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nx">conn</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">conn</span><span class="p">.</span><span class="nx">Close</span><span class="p">()</span>
<span class="w"> </span><span class="k">break</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">n</span><span class="o">++</span>
<span class="w"> </span><span class="nx">time</span><span class="p">.</span><span class="nx">Sleep</span><span class="p">(</span><span class="mi">10</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nx">time</span><span class="p">.</span><span class="nx">Millisecond</span><span class="p">)</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
<span class="kd">func</span><span class="w"> </span><span class="nx">startFluxbox</span><span class="p">()</span><span class="w"> </span><span class="o">*</span><span class="nx">exec</span><span class="p">.</span><span class="nx">Cmd</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">fluxbox</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">exec</span><span class="p">.</span><span class="nx">Command</span><span class="p">(</span><span class="s">"fluxbox"</span><span class="p">)</span>
<span class="w"> </span><span class="nx">fmt</span><span class="p">.</span><span class="nx">Println</span><span class="p">(</span><span class="s">"Starting fluxbox"</span><span class="p">)</span>
<span class="w"> </span><span class="nx">fluxbox</span><span class="p">.</span><span class="nx">Start</span><span class="p">()</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">fluxbox</span>
<span class="p">}</span>
<span class="kd">func</span><span class="w"> </span><span class="nx">printSeleniumCombinedOutput</span><span class="p">(</span><span class="nx">seleniumStdout</span><span class="w"> </span><span class="nx">io</span><span class="p">.</span><span class="nx">ReadCloser</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">scanner</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">bufio</span><span class="p">.</span><span class="nx">NewScanner</span><span class="p">(</span><span class="nx">seleniumStdout</span><span class="p">)</span>
<span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="nx">scanner</span><span class="p">.</span><span class="nx">Scan</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">line</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">scanner</span><span class="p">.</span><span class="nx">Text</span><span class="p">()</span>
<span class="w"> </span><span class="nx">fmt</span><span class="p">.</span><span class="nx">Println</span><span class="p">(</span><span class="nx">line</span><span class="p">)</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
<span class="kd">func</span><span class="w"> </span><span class="nx">startSelenium</span><span class="p">()</span><span class="w"> </span><span class="o">*</span><span class="nx">exec</span><span class="p">.</span><span class="nx">Cmd</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">fmt</span><span class="p">.</span><span class="nx">Println</span><span class="p">(</span><span class="s">"Starting selenium standalone"</span><span class="p">)</span>
<span class="w"> </span><span class="nx">selenium</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">exec</span><span class="p">.</span><span class="nx">Command</span><span class="p">(</span><span class="s">"java"</span><span class="p">,</span><span class="w"> </span><span class="s">"-jar"</span><span class="p">,</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nx">Getenv</span><span class="p">(</span><span class="s">"SELENIUM_PATH"</span><span class="p">),</span><span class="w"> </span><span class="s">"-port"</span><span class="p">,</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nx">Getenv</span><span class="p">(</span><span class="s">"SELENIUM_PORT"</span><span class="p">))</span>
<span class="w"> </span><span class="nx">seleniumStdout</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">selenium</span><span class="p">.</span><span class="nx">StdoutPipe</span><span class="p">()</span>
<span class="w"> </span><span class="nx">selenium</span><span class="p">.</span><span class="nx">Stderr</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">selenium</span><span class="p">.</span><span class="nx">Stdout</span>
<span class="w"> </span><span class="k">go</span><span class="w"> </span><span class="nx">printSeleniumCombinedOutput</span><span class="p">(</span><span class="nx">seleniumStdout</span><span class="p">)</span>
<span class="w"> </span><span class="nx">selenium</span><span class="p">.</span><span class="nx">Start</span><span class="p">()</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">selenium</span>
<span class="p">}</span>
<span class="kd">func</span><span class="w"> </span><span class="nx">startProcesses</span><span class="p">()</span><span class="w"> </span><span class="p">(</span><span class="o">*</span><span class="nx">exec</span><span class="p">.</span><span class="nx">Cmd</span><span class="p">,</span><span class="w"> </span><span class="o">*</span><span class="nx">exec</span><span class="p">.</span><span class="nx">Cmd</span><span class="p">,</span><span class="w"> </span><span class="o">*</span><span class="nx">exec</span><span class="p">.</span><span class="nx">Cmd</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">xvnc</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">startXvnc</span><span class="p">()</span>
<span class="w"> </span><span class="nx">waitForPort</span><span class="p">()</span>
<span class="w"> </span><span class="nx">fluxbox</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">startFluxbox</span><span class="p">()</span>
<span class="w"> </span><span class="nx">selenium</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">startSelenium</span><span class="p">()</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">xvnc</span><span class="p">,</span><span class="w"> </span><span class="nx">fluxbox</span><span class="p">,</span><span class="w"> </span><span class="nx">selenium</span>
<span class="p">}</span>
<span class="kd">func</span><span class="w"> </span><span class="nx">waitForSignals</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">sigs</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nx">Signal</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span>
<span class="w"> </span><span class="nx">signal</span><span class="p">.</span><span class="nx">Notify</span><span class="p">(</span><span class="nx">sigs</span><span class="p">,</span><span class="w"> </span><span class="nx">syscall</span><span class="p">.</span><span class="nx">SIGINT</span><span class="p">,</span><span class="w"> </span><span class="nx">syscall</span><span class="p">.</span><span class="nx">SIGTERM</span><span class="p">)</span>
<span class="w"> </span><span class="o"><-</span><span class="nx">sigs</span>
<span class="p">}</span>
<span class="kd">func</span><span class="w"> </span><span class="nx">stopProcesses</span><span class="p">(</span><span class="nx">xvnc</span><span class="w"> </span><span class="o">*</span><span class="nx">exec</span><span class="p">.</span><span class="nx">Cmd</span><span class="p">,</span><span class="w"> </span><span class="nx">fluxbox</span><span class="w"> </span><span class="o">*</span><span class="nx">exec</span><span class="p">.</span><span class="nx">Cmd</span><span class="p">,</span><span class="w"> </span><span class="nx">selenium</span><span class="w"> </span><span class="o">*</span><span class="nx">exec</span><span class="p">.</span><span class="nx">Cmd</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">fmt</span><span class="p">.</span><span class="nx">Println</span><span class="p">(</span><span class="s">"Stopping selenium"</span><span class="p">)</span>
<span class="w"> </span><span class="nx">selenium</span><span class="p">.</span><span class="nx">Process</span><span class="p">.</span><span class="nx">Kill</span><span class="p">()</span>
<span class="w"> </span><span class="nx">selenium</span><span class="p">.</span><span class="nx">Wait</span><span class="p">()</span>
<span class="w"> </span><span class="nx">fmt</span><span class="p">.</span><span class="nx">Println</span><span class="p">(</span><span class="s">"Stopping fluxbox"</span><span class="p">)</span>
<span class="w"> </span><span class="nx">fluxbox</span><span class="p">.</span><span class="nx">Process</span><span class="p">.</span><span class="nx">Kill</span><span class="p">()</span>
<span class="w"> </span><span class="nx">fluxbox</span><span class="p">.</span><span class="nx">Wait</span><span class="p">()</span>
<span class="w"> </span><span class="nx">fmt</span><span class="p">.</span><span class="nx">Println</span><span class="p">(</span><span class="s">"Stopping Xvnc"</span><span class="p">)</span>
<span class="w"> </span><span class="nx">xvnc</span><span class="p">.</span><span class="nx">Process</span><span class="p">.</span><span class="nx">Kill</span><span class="p">()</span>
<span class="w"> </span><span class="nx">xvnc</span><span class="p">.</span><span class="nx">Wait</span><span class="p">()</span>
<span class="p">}</span>
<span class="kd">func</span><span class="w"> </span><span class="nx">main</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">xvnc</span><span class="p">,</span><span class="w"> </span><span class="nx">fluxbox</span><span class="p">,</span><span class="w"> </span><span class="nx">selenium</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">startProcesses</span><span class="p">()</span>
<span class="w"> </span><span class="nx">waitForSignals</span><span class="p">()</span>
<span class="w"> </span><span class="nx">stopProcesses</span><span class="p">(</span><span class="nx">xvnc</span><span class="p">,</span><span class="w"> </span><span class="nx">fluxbox</span><span class="p">,</span><span class="w"> </span><span class="nx">selenium</span><span class="p">)</span>
<span class="w"> </span><span class="nx">fmt</span><span class="p">.</span><span class="nx">Println</span><span class="p">(</span><span class="s">"Bye bye"</span><span class="p">)</span>
<span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nx">Exit</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div>
<p>I like Go for its simplicity and one binary output. This small init program solved the issue
with <code>fluxbox</code>. Using multi-stage image building we can compile the init program in of the
preliminary stages:</p>
<div class="highlight"><pre><span></span><code><span class="k">FROM</span><span class="w"> </span><span class="s">docker.io/library/golang:1.17</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">builder</span>
<span class="k">COPY</span><span class="w"> </span>init.go<span class="w"> </span>.
<span class="k">RUN</span><span class="w"> </span>go<span class="w"> </span>build<span class="w"> </span>init.go
<span class="k">FROM</span><span class="w"> </span><span class="s">quay.io/redhatqe/selenium-base:latest</span>
<span class="k">COPY</span><span class="w"> </span>--from<span class="o">=</span>builder<span class="w"> </span>/go/init<span class="w"> </span>/usr/bin/
<span class="k">RUN</span><span class="w"> </span>chmod<span class="w"> </span>+x<span class="w"> </span>/usr/bin/init
<span class="k">CMD</span><span class="w"> </span><span class="p">[</span><span class="s2">"init"</span><span class="p">]</span>
</code></pre></div>
<h3>Results</h3>
<p>Let's compare image sizes:</p>
<div class="highlight"><pre><span></span><code>podman<span class="w"> </span>pull<span class="w"> </span>docker.io/selenium/standalone-firefox:latest<span class="w"> </span>quay.io/redhatqe/selenium-standalone:latest
podman<span class="w"> </span>images
REPOSITORY<span class="w"> </span>TAG<span class="w"> </span>IMAGE<span class="w"> </span>ID<span class="w"> </span>CREATED<span class="w"> </span>SIZE
docker.io/selenium/standalone-firefox<span class="w"> </span>latest<span class="w"> </span>d1f4408519cd<span class="w"> </span><span class="m">3</span><span class="w"> </span>days<span class="w"> </span>ago<span class="w"> </span><span class="m">998</span><span class="w"> </span>MB
quay.io/redhatqe/selenium-standalone<span class="w"> </span>latest<span class="w"> </span>2562f5b2819d<span class="w"> </span><span class="m">10</span><span class="w"> </span>days<span class="w"> </span>ago<span class="w"> </span><span class="m">902</span><span class="w"> </span>MB
</code></pre></div>
<p>As you can see <code>quay.io/redhatqe/selenium-standalone:latest</code> has lesser size than
<code>docker.io/selenium/standalone-firefox:latest</code>. Keep in mind that our image contains both Firefox
and Chrome browsers. I think it makes sense to have a separate image per a browser. This will
give us thinner images.</p>
<p>References:</p>
<ul>
<li><a href="https://github.com/RedHatQE/selenium-images">https://github.com/RedHatQE/selenium-images</a></li>
<li><a href="https://quay.io/repository/redhatqe/selenium-standalone">https://quay.io/repository/redhatqe/selenium-standalone</a></li>
<li><a href="https://quay.io/repository/redhatqe/selenium-base">https://quay.io/repository/redhatqe/selenium-base</a></li>
<li><a href="https://github.com/SeleniumHQ/docker-selenium">https://github.com/SeleniumHQ/docker-selenium</a></li>
</ul>
<p><a href="https://github.com/quarckster/blog.misharov.pro/discussions/21">Discuss on Github</a></p>GitLab custom executor for Openstack2021-06-18T00:00:00+02:002021-06-18T00:00:00+02:00tag:blog.misharov.pro,2021-06-18:/2021-06-18/gitlab-openstack-executorGitlab CI doesn't have built-in support of Openstack but provides the API to add such support via
Drivers. In this post I'll demonstrate how to create such drivers.<p>Gitlab CI doesn't have built-in support of Openstack but provides the API to add such support via
<code>Drivers</code>. In this post I'll demonstrate how to create such drivers.</p>
<h2>Preamble</h2>
<p>I've already written about Gitlab CI here. In particular in
<a href="https://blog.misharov.pro/2020-05-08/gitlab-runner-in-openshift">Gitlab runner in Openshift</a>. I shared my
experience with using Gitlab CI and Kubernetes executor. In some cases, containers don't fit your
workflows and you would like to run Gitlab CI jobs on real VMs. Gitlab CI provides various executors
including <code>shell</code>, <code>ssh</code>, <code>parallels</code> and <code>virtualbox</code> that use virtual machines as an environment
for job execution. The problem with <code>shell</code> and <code>ssh</code> executors that they don't provide a clean
environment. There might be undesired side-effects and leftovers from prior jobs. As for <code>parallels</code>
and <code>virtualbox</code> executors they look exotic and a bit outdated solutions. They're just not scalable.
It would much better to leverage the power of one of the cloud providers to provision VMs for our
Gitlab CI jobs.</p>
<h2>Openstack</h2>
<p>OpenStack is a free, open standard cloud computing platform. It has REST API to manage instances and
that's exactly what we need for our driver. Moreover, Openstack project provides CLI utility and
Python SDK. We will use it for creating an Openstack driver for Gitlab CI.</p>
<div class="highlight"><pre><span></span><code>pip<span class="w"> </span>install<span class="w"> </span>openstacksdk
</code></pre></div>
<h2>Custom executor</h2>
<p>According the documentation to set up a custom executor for <code>gitlab-runner</code> we should provide the
following config:</p>
<div class="highlight"><pre><span></span><code><span class="k">[[runners]]</span>
<span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"custom"</span>
<span class="w"> </span><span class="n">url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"https://gitlab.com"</span>
<span class="w"> </span><span class="n">token</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"TOKEN"</span>
<span class="w"> </span><span class="n">executor</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"custom"</span>
<span class="w"> </span><span class="n">builds_dir</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"/builds"</span>
<span class="w"> </span><span class="n">cache_dir</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"/cache"</span>
<span class="w"> </span><span class="k">[runners.custom]</span>
<span class="w"> </span><span class="n">config_exec</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"/path/to/config_executable"</span><span class="w"> </span><span class="c1"># optional</span>
<span class="w"> </span><span class="n">config_args</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s">"SomeArg"</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="c1"># optional</span>
<span class="w"> </span><span class="n">config_exec_timeout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">200</span><span class="w"> </span><span class="c1"># optional</span>
<span class="w"> </span><span class="n">prepare_exec</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"/path/to/prepare_executable"</span>
<span class="w"> </span><span class="n">prepare_args</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s">"SomeArg"</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="c1"># optional</span>
<span class="w"> </span><span class="n">prepare_exec_timeout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">200</span><span class="w"> </span><span class="c1"># optional</span>
<span class="w"> </span><span class="n">run_exec</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"/path/to/run_executable"</span>
<span class="w"> </span><span class="n">run_args</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s">"SomeArg"</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="c1"># optional</span>
<span class="w"> </span><span class="n">cleanup_exec</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"/path/to/cleanup_executable"</span>
<span class="w"> </span><span class="n">cleanup_args</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s">"SomeArg"</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="c1"># optional</span>
<span class="w"> </span><span class="n">cleanup_exec_timeout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">200</span><span class="w"> </span><span class="c1"># optional</span>
<span class="w"> </span><span class="n">graceful_kill_timeout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">200</span><span class="w"> </span><span class="c1"># optional</span>
<span class="w"> </span><span class="n">force_kill_timeout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">200</span><span class="w"> </span><span class="c1"># optional</span>
</code></pre></div>
<p>As you can see each Gitlab CI job contains four stages: <code>config</code>, <code>prepare</code>, <code>run</code> and <code>cleanup</code>.
Our task is to create scripts for corresponding stages.</p>
<h2>Config</h2>
<p>It's not a mandatory stage but it's nice to have. An executable in this stage should print to stdout
a JSON string with specific keys. In my case the executable is a simple shell script:</p>
<div class="highlight"><pre><span></span><code><span class="ch">#!/usr/bin/env bash</span>
cat<span class="w"> </span><span class="s"><< EOS</span>
<span class="s">{</span>
<span class="s"> "driver": {</span>
<span class="s"> "name": "Openstack",</span>
<span class="s"> "version": "0.0.1"</span>
<span class="s"> }</span>
<span class="s">}</span>
<span class="s">EOS</span>
</code></pre></div>
<p>After adding this to <code>config_exec</code> job logs are started from these lines:</p>
<div class="highlight"><pre><span></span><code>Running with gitlab-runner 13.12.0 (7a6612da)
on openstack hkLsofs5
Preparing the "custom" executor
Using Custom executor with driver Openstack 0.0.1...
</code></pre></div>
<h2>Prepare</h2>
<p>Here we need to provision a VM where the job scripts will be executed. We will use python and
<code>openstacksdk</code> package. Besides we will need <code>paramiko</code> package. It's a well-known python
implementation of SSH for both the server and the client. We use <code>paramiko</code> in this stage to check
if the VM is ready to accept commands via SSH. Here is the script I've created:</p>
<div class="highlight"><pre><span></span><code><span class="ch">#!/usr/bin/env python</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">import</span> <span class="nn">traceback</span>
<span class="kn">import</span> <span class="nn">openstack</span>
<span class="kn">import</span> <span class="nn">paramiko</span>
<span class="c1"># a module that contains required parameters to set up VMs and an SSH connection.</span>
<span class="kn">import</span> <span class="nn">env</span>
<span class="k">def</span> <span class="nf">provision_server</span><span class="p">(</span><span class="n">conn</span><span class="p">:</span> <span class="n">openstack</span><span class="o">.</span><span class="n">connection</span><span class="o">.</span><span class="n">Connection</span><span class="p">)</span> <span class="o">-></span> <span class="n">openstack</span><span class="o">.</span><span class="n">compute</span><span class="o">.</span><span class="n">v2</span><span class="o">.</span><span class="n">server</span><span class="o">.</span><span class="n">Server</span><span class="p">:</span>
<span class="n">image</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">compute</span><span class="o">.</span><span class="n">find_image</span><span class="p">(</span><span class="n">env</span><span class="o">.</span><span class="n">BUILDER_IMAGE</span><span class="p">)</span>
<span class="n">flavor</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">compute</span><span class="o">.</span><span class="n">find_flavor</span><span class="p">(</span><span class="n">env</span><span class="o">.</span><span class="n">FLAVOR</span><span class="p">)</span>
<span class="n">network</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">network</span><span class="o">.</span><span class="n">find_network</span><span class="p">(</span><span class="n">env</span><span class="o">.</span><span class="n">NETWORK</span><span class="p">)</span>
<span class="n">server</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">compute</span><span class="o">.</span><span class="n">create_server</span><span class="p">(</span>
<span class="n">name</span><span class="o">=</span><span class="n">env</span><span class="o">.</span><span class="n">VM_NAME</span><span class="p">,</span>
<span class="n">flavor_id</span><span class="o">=</span><span class="n">flavor</span><span class="o">.</span><span class="n">id</span><span class="p">,</span>
<span class="n">image_id</span><span class="o">=</span><span class="n">image</span><span class="o">.</span><span class="n">id</span><span class="p">,</span>
<span class="n">key_name</span><span class="o">=</span><span class="n">env</span><span class="o">.</span><span class="n">KEY_PAIR_NAME</span><span class="p">,</span>
<span class="n">security_groups</span><span class="o">=</span><span class="p">[{</span><span class="s2">"name"</span><span class="p">:</span> <span class="n">env</span><span class="o">.</span><span class="n">SECURITY_GROUP</span><span class="p">}],</span>
<span class="n">networks</span><span class="o">=</span><span class="p">[{</span><span class="s2">"uuid"</span><span class="p">:</span> <span class="n">network</span><span class="o">.</span><span class="n">id</span><span class="p">}],</span>
<span class="p">)</span>
<span class="k">return</span> <span class="n">conn</span><span class="o">.</span><span class="n">compute</span><span class="o">.</span><span class="n">wait_for_server</span><span class="p">(</span><span class="n">server</span><span class="p">,</span> <span class="n">wait</span><span class="o">=</span><span class="mi">600</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">get_server_ip</span><span class="p">(</span>
<span class="n">conn</span><span class="p">:</span> <span class="n">openstack</span><span class="o">.</span><span class="n">connection</span><span class="o">.</span><span class="n">Connection</span><span class="p">,</span> <span class="n">server</span><span class="p">:</span> <span class="n">openstack</span><span class="o">.</span><span class="n">compute</span><span class="o">.</span><span class="n">v2</span><span class="o">.</span><span class="n">server</span><span class="o">.</span><span class="n">Server</span>
<span class="p">)</span> <span class="o">-></span> <span class="nb">str</span><span class="p">:</span>
<span class="k">return</span> <span class="nb">list</span><span class="p">(</span><span class="n">conn</span><span class="o">.</span><span class="n">compute</span><span class="o">.</span><span class="n">server_ips</span><span class="p">(</span><span class="n">server</span><span class="p">))[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">address</span>
<span class="k">def</span> <span class="nf">check_ssh</span><span class="p">(</span><span class="n">ip</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-></span> <span class="kc">None</span><span class="p">:</span>
<span class="n">ssh_client</span> <span class="o">=</span> <span class="n">paramiko</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">SSHClient</span><span class="p">()</span>
<span class="c1"># RSASHA256Key is not a part of mainline paramiko. I took it from this PR</span>
<span class="c1"># https://github.com/paramiko/paramiko/pull/1643.</span>
<span class="n">pkey</span> <span class="o">=</span> <span class="n">paramiko</span><span class="o">.</span><span class="n">rsakey</span><span class="o">.</span><span class="n">RSASHA256Key</span><span class="o">.</span><span class="n">from_private_key_file</span><span class="p">(</span><span class="n">env</span><span class="o">.</span><span class="n">PRIVATE_KEY_PATH</span><span class="p">)</span>
<span class="n">ssh_client</span><span class="o">.</span><span class="n">set_missing_host_key_policy</span><span class="p">(</span><span class="n">paramiko</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">AutoAddPolicy</span><span class="p">())</span>
<span class="n">ssh_client</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span>
<span class="n">hostname</span><span class="o">=</span><span class="n">ip</span><span class="p">,</span>
<span class="n">username</span><span class="o">=</span><span class="n">env</span><span class="o">.</span><span class="n">USERNAME</span><span class="p">,</span>
<span class="n">pkey</span><span class="o">=</span><span class="n">pkey</span><span class="p">,</span>
<span class="n">look_for_keys</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">allow_agent</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">timeout</span><span class="o">=</span><span class="mi">60</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">ssh_client</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">()</span> <span class="o">-></span> <span class="kc">None</span><span class="p">:</span>
<span class="nb">print</span><span class="p">(</span><span class="s2">"Connecting to Openstack"</span><span class="p">,</span> <span class="n">flush</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">conn</span> <span class="o">=</span> <span class="n">openstack</span><span class="o">.</span><span class="n">connect</span><span class="p">()</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Provisioning an instance </span><span class="si">{</span><span class="n">env</span><span class="o">.</span><span class="n">VM_NAME</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span> <span class="n">flush</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">server</span> <span class="o">=</span> <span class="n">provision_server</span><span class="p">(</span><span class="n">conn</span><span class="p">)</span>
<span class="n">ip</span> <span class="o">=</span> <span class="n">get_server_ip</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Instance </span><span class="si">{</span><span class="n">env</span><span class="o">.</span><span class="n">VM_NAME</span><span class="si">}</span><span class="s2"> is running on address </span><span class="si">{</span><span class="n">ip</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span> <span class="n">flush</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">conn</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
<span class="nb">print</span><span class="p">(</span><span class="s2">"Checking SSH connection"</span><span class="p">,</span> <span class="n">flush</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">check_ssh</span><span class="p">(</span><span class="n">ip</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="s2">"SSH connection has been established"</span><span class="p">,</span> <span class="n">flush</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
<span class="n">traceback</span><span class="o">.</span><span class="n">print_exc</span><span class="p">()</span>
<span class="c1"># gitlab-runner expects a certain exit code in case of failure.</span>
<span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">env</span><span class="o">.</span><span class="n">SYSTEM_FAILURE_EXIT_CODE</span><span class="p">))</span>
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">"__main__"</span><span class="p">:</span>
<span class="n">main</span><span class="p">()</span>
</code></pre></div>
<p>I believe it's really straightforward and it's clear what the script does. But there are a couple of
moments I'd like to highlight.</p>
<h3>Paramiko and RSA keys</h3>
<p>When I was playing with <code>paramiko</code> to test how it works with VMs SSH I got <code>Authentication error</code>
exception. It was very confusing because the OpenSSH client connected without any issue with the
same private key. I even generated a key using <code>paramiko</code> and then tried to authenticate but no
luck. I found out that <code>paramiko</code> doesn't support modern RSA algorithms. There is a
<a href="https://github.com/paramiko/paramiko/pull/1643">PR</a> that adds this functionality and I had to build
and install <code>paramiko</code> from it in order to get it working.</p>
<div class="highlight"><pre><span></span><code>pip<span class="w"> </span>install<span class="w"> </span>-U<span class="w"> </span>git+https://github.com/kkovaacs/paramiko.git@rsa-sha2-algorithms
</code></pre></div>
<h3>print() flush</h3>
<p>During the testing, I noticed that job logs are displayed in Gitlab UI not immediately but only
when the script finished. That was the strange and undesired effect. The issue was in the default
behavior of the python <code>print()</code> function. For sake of performance reasons stdout in python is
buffered. The output will be emitted only when the buffer is filled. In order to forcibly <em>flush</em>
the buffer <code>print()</code> has boolean <code>flush</code> parameter. When I set <code>flush=True</code> in all <code>print()</code> calls
job logs in Gitlab UI got live streaming.</p>
<h2>Run</h2>
<p>A VM is provisioned and we can connect to it via SSH. Now we can execute some commands and scripts.
<code>gitlab-runner</code> generates shell scripts from job definitions then it dumps them to the file system
and passes a path as an argument to our <code>run</code> executable. Our <code>run.py</code> should read that script and
send its content directly to the stdin of some shell interpreter. In my case it's <code>/bin/bash</code>. Here
is the content of <code>run.py</code>:</p>
<div class="highlight"><pre><span></span><code><span class="ch">#!/usr/bin/env python</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">import</span> <span class="nn">openstack</span>
<span class="kn">import</span> <span class="nn">paramiko</span>
<span class="kn">import</span> <span class="nn">env</span>
<span class="k">def</span> <span class="nf">get_server_ip</span><span class="p">(</span><span class="n">conn</span><span class="p">:</span> <span class="n">openstack</span><span class="o">.</span><span class="n">connection</span><span class="o">.</span><span class="n">Connection</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span><span class="p">:</span>
<span class="n">server</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">conn</span><span class="o">.</span><span class="n">compute</span><span class="o">.</span><span class="n">servers</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="n">env</span><span class="o">.</span><span class="n">VM_NAME</span><span class="p">,</span> <span class="n">status</span><span class="o">=</span><span class="s2">"ACTIVE"</span><span class="p">))[</span><span class="mi">0</span><span class="p">]</span>
<span class="k">return</span> <span class="nb">list</span><span class="p">(</span><span class="n">conn</span><span class="o">.</span><span class="n">compute</span><span class="o">.</span><span class="n">server_ips</span><span class="p">(</span><span class="n">server</span><span class="p">))[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">address</span>
<span class="k">def</span> <span class="nf">execute_script_on_server</span><span class="p">(</span><span class="n">ssh</span><span class="p">:</span> <span class="n">paramiko</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">SSHClient</span><span class="p">,</span> <span class="n">script_path</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-></span> <span class="nb">int</span><span class="p">:</span>
<span class="c1"># paramiko's exec_command() returns a tuple of stdin, stdout, stderr</span>
<span class="c1"># file-like objects</span>
<span class="n">stdin</span><span class="p">,</span> <span class="n">stdout</span><span class="p">,</span> <span class="n">stderr</span> <span class="o">=</span> <span class="n">ssh</span><span class="o">.</span><span class="n">exec_command</span><span class="p">(</span><span class="s2">"/bin/bash"</span><span class="p">)</span>
<span class="c1"># Read the script content and send it to the remote bash stdin.</span>
<span class="c1"># We emulate this shell command:</span>
<span class="c1"># ssh user@host /bin/bash < script.sh</span>
<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">script_path</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
<span class="n">stdin</span><span class="o">.</span><span class="n">channel</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="n">f</span><span class="o">.</span><span class="n">read</span><span class="p">())</span>
<span class="n">stdin</span><span class="o">.</span><span class="n">channel</span><span class="o">.</span><span class="n">shutdown_write</span><span class="p">()</span>
<span class="c1"># Here we read the output line by line but only first 2048 bytes. It</span>
<span class="c1"># prevents possible overflows if a line is huge.</span>
<span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="nb">iter</span><span class="p">(</span><span class="k">lambda</span><span class="p">:</span> <span class="n">stdout</span><span class="o">.</span><span class="n">readline</span><span class="p">(</span><span class="mi">2048</span><span class="p">),</span> <span class="s2">""</span><span class="p">):</span>
<span class="c1"># Don't forget to flush the buffer for live log streaming</span>
<span class="nb">print</span><span class="p">(</span><span class="n">line</span><span class="p">,</span> <span class="n">sep</span><span class="o">=</span><span class="s2">""</span><span class="p">,</span> <span class="n">end</span><span class="o">=</span><span class="s2">""</span><span class="p">,</span> <span class="n">flush</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="c1"># When the script execution finished we save the exit code.</span>
<span class="n">exit_status</span> <span class="o">=</span> <span class="n">stdout</span><span class="o">.</span><span class="n">channel</span><span class="o">.</span><span class="n">recv_exit_status</span><span class="p">()</span>
<span class="k">if</span> <span class="n">exit_status</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">:</span>
<span class="c1"># In case of non 0 exit code print stderr as well.</span>
<span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="nb">iter</span><span class="p">(</span><span class="k">lambda</span><span class="p">:</span> <span class="n">stderr</span><span class="o">.</span><span class="n">readline</span><span class="p">(</span><span class="mi">2048</span><span class="p">),</span> <span class="s2">""</span><span class="p">):</span>
<span class="nb">print</span><span class="p">(</span><span class="n">line</span><span class="p">,</span> <span class="n">sep</span><span class="o">=</span><span class="s2">""</span><span class="p">,</span> <span class="n">end</span><span class="o">=</span><span class="s2">""</span><span class="p">,</span> <span class="n">flush</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="k">return</span> <span class="n">exit_status</span>
<span class="k">def</span> <span class="nf">get_ssh_client</span><span class="p">(</span><span class="n">ip</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-></span> <span class="n">paramiko</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">SSHClient</span><span class="p">:</span>
<span class="n">ssh_client</span> <span class="o">=</span> <span class="n">paramiko</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">SSHClient</span><span class="p">()</span>
<span class="n">pkey</span> <span class="o">=</span> <span class="n">paramiko</span><span class="o">.</span><span class="n">rsakey</span><span class="o">.</span><span class="n">RSASHA256Key</span><span class="o">.</span><span class="n">from_private_key_file</span><span class="p">(</span><span class="n">env</span><span class="o">.</span><span class="n">PRIVATE_KEY_PATH</span><span class="p">)</span>
<span class="n">ssh_client</span><span class="o">.</span><span class="n">set_missing_host_key_policy</span><span class="p">(</span><span class="n">paramiko</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">AutoAddPolicy</span><span class="p">())</span>
<span class="n">ssh_client</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span>
<span class="n">hostname</span><span class="o">=</span><span class="n">ip</span><span class="p">,</span>
<span class="n">username</span><span class="o">=</span><span class="n">env</span><span class="o">.</span><span class="n">USERNAME</span><span class="p">,</span>
<span class="n">pkey</span><span class="o">=</span><span class="n">pkey</span><span class="p">,</span>
<span class="n">look_for_keys</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">allow_agent</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">timeout</span><span class="o">=</span><span class="mi">60</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">return</span> <span class="n">ssh_client</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">()</span> <span class="o">-></span> <span class="kc">None</span><span class="p">:</span>
<span class="n">conn</span> <span class="o">=</span> <span class="n">openstack</span><span class="o">.</span><span class="n">connect</span><span class="p">()</span>
<span class="n">ip</span> <span class="o">=</span> <span class="n">get_server_ip</span><span class="p">(</span><span class="n">conn</span><span class="p">)</span>
<span class="n">ssh_client</span> <span class="o">=</span> <span class="n">get_ssh_client</span><span class="p">(</span><span class="n">ip</span><span class="p">)</span>
<span class="c1"># gitlab-runner passes a path to the script in the first argument. We read</span>
<span class="c1"># that value in sys.argv[1]</span>
<span class="n">exit_status</span> <span class="o">=</span> <span class="n">execute_script_on_server</span><span class="p">(</span><span class="n">ssh_client</span><span class="p">,</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
<span class="n">ssh_client</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
<span class="k">if</span> <span class="n">exit_status</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">:</span>
<span class="c1"># gitlab-runner expects a certain exit code in case of a build failure.</span>
<span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">env</span><span class="o">.</span><span class="n">BUILD_FAILURE_EXIT_CODE</span><span class="p">))</span>
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">"__main__"</span><span class="p">:</span>
<span class="n">main</span><span class="p">()</span>
</code></pre></div>
<h2>Clean up</h2>
<p>Let me quote the official docs:</p>
<blockquote>
<p>This final stage is executed even if one of the previous stages failed.
The main goal for this stage is to clean up any of the environments that might
have been set up. For example, turning off VMs or deleting containers.</p>
</blockquote>
<p><code>cleanup.py</code> is simple:</p>
<div class="highlight"><pre><span></span><code><span class="ch">#!/usr/bin/env python</span>
<span class="kn">import</span> <span class="nn">openstack</span>
<span class="kn">import</span> <span class="nn">env</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">()</span> <span class="o">-></span> <span class="kc">None</span><span class="p">:</span>
<span class="n">conn</span> <span class="o">=</span> <span class="n">openstack</span><span class="o">.</span><span class="n">connect</span><span class="p">()</span>
<span class="c1"># During the prepare stage several VMs with the same name might be created.</span>
<span class="c1"># Delete them all.</span>
<span class="k">for</span> <span class="n">server</span> <span class="ow">in</span> <span class="n">conn</span><span class="o">.</span><span class="n">compute</span><span class="o">.</span><span class="n">servers</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="n">env</span><span class="o">.</span><span class="n">VM_NAME</span><span class="p">):</span>
<span class="n">conn</span><span class="o">.</span><span class="n">compute</span><span class="o">.</span><span class="n">delete_server</span><span class="p">(</span><span class="n">server</span><span class="p">)</span>
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">"__main__"</span><span class="p">:</span>
<span class="n">main</span><span class="p">()</span>
</code></pre></div>
<h2>Sequence diagrams</h2>
<p>Here are sequence diagram of the workflow:</p>
<p><img class="image-center" alt="diagram 1" src="https://blog.misharov.pro/assets/img/2021-06-18-gitlab-openstack-executor-1.png"/></p>
<p><img class="image-center" alt="diagram 2" src="https://blog.misharov.pro/assets/img/2021-06-18-gitlab-openstack-executor-2.png"/></p>
<h2>Distribution</h2>
<p>I hope you got the idea of how to create your own driver for the custom executor. But
I'd like to go further. It would be also nice to pack <code>gitlab-runner</code> with our
scripts into a container image. Later it can be deployed on a VM or
a container orchestration system such as Kubernetes. I prefer to build
<code>gitlab-runner</code> from the sources and using the multistage building. Here you will
find the content of the <a href="https://github.com/RedHatQE/openstack-gitlab-executor/blob/master/Containerfile">Containerfile</a>.
You can pull the latest prebuilt image from <code>quay.io/redhatqe/openstack-gitlab-runner:latest</code>.</p>
<h2>Usage</h2>
<p>Just pull the image and run a container with some environment variables. They
are required to set up connections to your Openstack and VM's SSH:</p>
<div class="highlight"><pre><span></span><code>podman<span class="w"> </span>run<span class="w"> </span>-it<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-e<span class="w"> </span><span class="nv">PRIVATE_KEY</span><span class="o">=</span><span class="s2">"</span><span class="k">$(</span>cat<span class="w"> </span><private<span class="w"> </span>key<span class="w"> </span>filename><span class="k">)</span><span class="s2">"</span>
<span class="w"> </span>--env-file<span class="o">=</span>env.txt<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>quay.io/redhatqe/openstack-gitlab-runner:latest
cat<span class="w"> </span>env.txt
<span class="nv">RUNNER_TAG_LIST</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">REGISTRATION_TOKEN</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">RUNNER_NAME</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">CI_SERVER_URL</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">RUNNER_BUILDS_DIR</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">RUNNER_CACHE_DIR</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">CONCURRENT</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">FLAVOR</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">BUILDER_IMAGE</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">NETWORK</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">KEY_PAIR_NAME</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">SECURITY_GROUP</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">USERNAME</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">OS_AUTH_URL</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">OS_PROJECT_NAME</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">OS_USERNAME</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">OS_PASSWORD</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">OS_PROJECT_DOMAIN_NAME</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">OS_USER_DOMAIN_NAME</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">OS_REGION_NAME</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">OS_IDENTITY_API_VERSION</span><span class="o">=</span><your<span class="w"> </span>value>
<span class="nv">OS_INTERFACE</span><span class="o">=</span><your<span class="w"> </span>value>
</code></pre></div>
<p>The full description of the environment variables and other details you will
find in the repository <a href="https://github.com/RedHatQE/openstack-gitlab-executor">https://github.com/RedHatQE/openstack-gitlab-executor</a>.</p>
<p>References:</p>
<ul>
<li><a href="https://github.com/RedHatQE/openstack-gitlab-executor">https://github.com/RedHatQE/openstack-gitlab-executor</a></li>
<li><a href="https://docs.gitlab.com/runner/executors/custom.html">https://docs.gitlab.com/runner/executors/custom.html</a></li>
<li><a href="https://docs.openstack.org/openstacksdk/latest/">https://docs.openstack.org/openstacksdk/latest/</a></li>
</ul>
<p><a href="https://github.com/quarckster/blog.misharov.pro/discussions/18">Discuss on Github</a></p>Building and deploying static websites using Openshift, S2I and Gitlab CI2021-05-23T00:00:00+02:002021-05-23T00:00:00+02:00tag:blog.misharov.pro,2021-05-23:/2021-05-23/s2i-static-sitesUsing Gitlab CI, Openshift and S2I technologies you can create a pipeline for building and deploying
static websites.<p>Using Gitlab CI, Openshift and S2I technologies you can create a pipeline for building and deploying
static websites.</p>
<h2>Prerequisites</h2>
<p>We have two repositories that are hosted on Gitlab:</p>
<ul>
<li><code>https://gitlab.example.com/data-script</code></li>
</ul>
<p>Contains a script that generates a bunch of JSON files</p>
<ul>
<li><code>https://gitlab.example.com/frontend</code></li>
</ul>
<p>Contains a React application that consumes JSON files</p>
<p>The web site is deployed on Openshift Container Platform, therefore we will leverage its features:</p>
<ul>
<li>Build Configs</li>
<li>S2I</li>
<li>Deployments (strictly speaking it's a native K8S resource)</li>
<li>Routes</li>
</ul>
<p>The task is to create a Gitlab CI pipeline that would build and deploy the static website when a tag
is pushed into one of the repositories.</p>
<p><img class="image-center" alt="diagram 1" src="https://blog.misharov.pro/assets/img/2021-05-23-s2i-static-sites-1.png"/></p>
<h2>Manifests</h2>
<h3>Dockerfiles</h3>
<p>We are going to deploy the web site on OCP and we need to build a container image that will contain
all required assets of our web sites. Let's start from <code>data-script</code>. During the image building we
need to generate a bunch JSON files. We need only JSONs and nothing else therefore we will use
multistaging building and <code>scrath</code> image:</p>
<div class="highlight"><pre><span></span><code><span class="k">FROM</span><span class="w"> </span><span class="s">registry.access.redhat.com/ubi8/ubi:8.4</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">builder</span>
<span class="k">ENV</span><span class="w"> </span><span class="nv">HOME</span><span class="o">=</span>/data_script/
<span class="k">WORKDIR</span><span class="w"> </span><span class="s">$HOME</span>
<span class="c"># install python</span>
<span class="k">RUN</span><span class="w"> </span>dnf<span class="w"> </span>install<span class="w"> </span>--nodocs<span class="w"> </span>-y<span class="w"> </span>python38<span class="w"> </span>python38-pip<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>python3<span class="w"> </span>-m<span class="w"> </span>venv<span class="w"> </span>/data_script_venv
<span class="k">ENV</span><span class="w"> </span><span class="nv">PATH</span><span class="o">=</span><span class="s2">"/data_script_venv/bin:</span><span class="nv">$PATH</span><span class="s2">"</span>
<span class="k">COPY</span><span class="w"> </span>.<span class="w"> </span>.
<span class="c"># generate JSON files</span>
<span class="k">RUN</span><span class="w"> </span>pip<span class="w"> </span>install<span class="w"> </span>-U<span class="w"> </span>--no-cache-dir<span class="w"> </span>pip<span class="w"> </span>setuptools<span class="w"> </span>wheel<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>pip<span class="w"> </span>install<span class="w"> </span>--no-cache-dir<span class="w"> </span>-r<span class="w"> </span>requirements.txt<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>mkdir<span class="w"> </span>jsons<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>python<span class="w"> </span>main.py
<span class="c"># we don't need anything, this container image is just an artifacts storage</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">scratch</span>
<span class="k">COPY</span><span class="w"> </span>--from<span class="o">=</span>builder<span class="w"> </span>/data_script/jsons<span class="w"> </span>/artifacts
</code></pre></div>
<p><code>frontend</code> follows the similar approach. We generate the assets and put them into a <code>scratch</code> image:</p>
<div class="highlight"><pre><span></span><code><span class="k">FROM</span><span class="w"> </span><span class="s">registry.access.redhat.com/ubi8/ubi:8.4</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">builder</span>
<span class="k">ENV</span><span class="w"> </span><span class="nv">HOME</span><span class="o">=</span>/frontend/
<span class="k">WORKDIR</span><span class="w"> </span><span class="s">$HOME</span>
<span class="k">COPY</span><span class="w"> </span>.<span class="w"> </span>.
<span class="c"># install nodejs and npm</span>
<span class="k">RUN</span><span class="w"> </span>dnf<span class="w"> </span>install<span class="w"> </span>--nodocs<span class="w"> </span>-y<span class="w"> </span>npm<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>npm<span class="w"> </span>install<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>npm<span class="w"> </span>run<span class="w"> </span>build<span class="w"> </span>
<span class="k">FROM</span><span class="w"> </span><span class="s">scratch</span>
<span class="k">COPY</span><span class="w"> </span>--from<span class="o">=</span>builder<span class="w"> </span>/frontend/dist<span class="w"> </span>/artifacts
</code></pre></div>
<h3>Build configs</h3>
<p><code>Build Config</code> is an Openshift resource that has instructions how to build an application. It has
various strategies but in our case we will use <code>Docker</code> and <code>Source</code> strategies. <code>Docker</code> strategy
is used for building artifacts images. Both of them are pretty the same:</p>
<div class="highlight"><pre><span></span><code><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">BuildConfig</span>
<span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">build.openshift.io/v1</span>
<span class="nt">metadata</span><span class="p">:</span>
<span class="w"> </span><span class="c1"># name: frontend-artifacts</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">json-artifacts</span>
<span class="w"> </span><span class="nt">labels</span><span class="p">:</span>
<span class="w"> </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">demo</span>
<span class="nt">spec</span><span class="p">:</span>
<span class="w"> </span><span class="nt">output</span><span class="p">:</span>
<span class="w"> </span><span class="nt">to</span><span class="p">:</span>
<span class="w"> </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">DockerImage</span>
<span class="w"> </span><span class="c1"># for frontend it would have this value</span>
<span class="w"> </span><span class="c1"># name: some-registry/static-website:frontend-artifacts</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">some-registry/static-website:data-artifacts</span>
<span class="w"> </span><span class="nt">pushSecret</span><span class="p">:</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">some-push-secret</span>
<span class="w"> </span><span class="c1"># it's a good practice to specify resource constraints</span>
<span class="w"> </span><span class="nt">resources</span><span class="p">:</span>
<span class="w"> </span><span class="nt">requests</span><span class="p">:</span>
<span class="w"> </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">256Mi</span>
<span class="w"> </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">250m</span>
<span class="w"> </span><span class="nt">limits</span><span class="p">:</span>
<span class="w"> </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">512Mi</span>
<span class="w"> </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">500m</span>
<span class="w"> </span><span class="nt">successfulBuildsHistoryLimit</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">1</span>
<span class="w"> </span><span class="nt">failedBuildsHistoryLimit</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">1</span>
<span class="w"> </span><span class="nt">strategy</span><span class="p">:</span>
<span class="w"> </span><span class="c1"># The quote from the Openshift documentation:</span>
<span class="w"> </span><span class="c1"># The docker build strategy invokes the docker build command, and it expects a repository with a</span>
<span class="w"> </span><span class="c1"># Dockerfile and all required artifacts in it to produce a runnable image</span>
<span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Docker</span>
<span class="w"> </span><span class="nt">source</span><span class="p">:</span>
<span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Git</span>
<span class="w"> </span><span class="nt">git</span><span class="p">:</span>
<span class="w"> </span><span class="c1"># frontend application lives here</span>
<span class="w"> </span><span class="c1"># uri: https://gitlab.example.com/frontend</span>
<span class="w"> </span><span class="nt">uri</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">https://gitlab.example.com/data-script</span>
</code></pre></div>
<p>The runnable image is built using <code>Source</code> strategy. This strategy uses so called S2I images in
order to produce runnable images without any <code>Dockerfile</code>. We will use
<code>registry.redhat.io/rhel8/nginx-118</code> S2I image for building a container with <code>nginx</code> that serves
static files from our artifacts images:</p>
<div class="highlight"><pre><span></span><code><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">BuildConfig</span>
<span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">build.openshift.io/v1</span>
<span class="nt">metadata</span><span class="p">:</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">runner</span>
<span class="w"> </span><span class="nt">labels</span><span class="p">:</span>
<span class="w"> </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">demo</span>
<span class="nt">spec</span><span class="p">:</span>
<span class="w"> </span><span class="nt">output</span><span class="p">:</span>
<span class="w"> </span><span class="nt">to</span><span class="p">:</span>
<span class="w"> </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">DockerImage</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">some-registry/static-website:runner</span>
<span class="w"> </span><span class="nt">pushSecret</span><span class="p">:</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">some-push-secret</span>
<span class="w"> </span><span class="nt">resources</span><span class="p">:</span>
<span class="w"> </span><span class="nt">requests</span><span class="p">:</span>
<span class="w"> </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">256Mi</span>
<span class="w"> </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">250m</span>
<span class="w"> </span><span class="nt">limits</span><span class="p">:</span>
<span class="w"> </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">512Mi</span>
<span class="w"> </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">500m</span>
<span class="w"> </span><span class="nt">successfulBuildsHistoryLimit</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">1</span>
<span class="w"> </span><span class="nt">failedBuildsHistoryLimit</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">1</span>
<span class="w"> </span><span class="nt">strategy</span><span class="p">:</span>
<span class="w"> </span><span class="c1"># During the building assets from artifacts images are copied inside</span>
<span class="w"> </span><span class="c1"># registry.redhat.io/rhel8/nginx-118 and s2i assembly scripts are executed</span>
<span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Source</span>
<span class="w"> </span><span class="nt">sourceStrategy</span><span class="p">:</span>
<span class="w"> </span><span class="nt">from</span><span class="p">:</span>
<span class="w"> </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">DockerImage</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">registry.redhat.io/rhel8/nginx-118</span>
<span class="w"> </span><span class="nt">pullSecret</span><span class="p">:</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">some-pull-secret</span>
<span class="w"> </span><span class="nt">source</span><span class="p">:</span>
<span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Image</span>
<span class="w"> </span><span class="nt">images</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">from</span><span class="p">:</span>
<span class="w"> </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">DockerImage</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">some-registry/static-website:data-artifacts</span>
<span class="w"> </span><span class="nt">paths</span><span class="p">:</span>
<span class="w"> </span><span class="c1"># we copy artifacts into the working directory</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">sourcePath</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">/artifacts</span>
<span class="w"> </span><span class="nt">destinationDir</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">.</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">from</span><span class="p">:</span>
<span class="w"> </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">DockerImage</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">some-registry/static-website:frontend-artifacts</span>
<span class="w"> </span><span class="nt">paths</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">sourcePath</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">/artifacts</span>
<span class="w"> </span><span class="nt">destinationDir</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">.</span>
<span class="w"> </span><span class="nt">runPolicy</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Serial</span>
</code></pre></div>
<h2>Deployment</h2>
<p>Here is <code>Delpoyment</code> manifest of our static web site:</p>
<div class="highlight"><pre><span></span><code><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Deployment</span>
<span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">apps/v1</span>
<span class="nt">metadata</span><span class="p">:</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">static-web-site</span>
<span class="w"> </span><span class="nt">labels</span><span class="p">:</span>
<span class="w"> </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">demo</span>
<span class="nt">spec</span><span class="p">:</span>
<span class="w"> </span><span class="nt">revisionHistoryLimit</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">2</span>
<span class="w"> </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">1</span>
<span class="w"> </span><span class="nt">strategy</span><span class="p">:</span>
<span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Recreate</span>
<span class="w"> </span><span class="nt">selector</span><span class="p">:</span>
<span class="w"> </span><span class="nt">matchLabels</span><span class="p">:</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">demo</span>
<span class="w"> </span><span class="nt">template</span><span class="p">:</span>
<span class="w"> </span><span class="nt">metadata</span><span class="p">:</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">demo</span>
<span class="w"> </span><span class="nt">labels</span><span class="p">:</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">demo</span>
<span class="w"> </span><span class="nt">spec</span><span class="p">:</span>
<span class="w"> </span><span class="nt">volumes</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">demo-config-map</span>
<span class="w"> </span><span class="nt">configMap</span><span class="p">:</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">demo</span>
<span class="w"> </span><span class="nt">defaultMode</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">420</span>
<span class="w"> </span><span class="nt">containers</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">runner</span>
<span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">some-registry/static-website:runner</span>
<span class="w"> </span><span class="c1"># this is important because the image tag is floating and we need to always pull the</span>
<span class="w"> </span><span class="c1"># image on every container spawning</span>
<span class="w"> </span><span class="nt">imagePullPolicy</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Always</span>
<span class="w"> </span><span class="nt">resources</span><span class="p">:</span>
<span class="w"> </span><span class="nt">limits</span><span class="p">:</span>
<span class="w"> </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">200m</span>
<span class="w"> </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">200Mi</span>
<span class="w"> </span><span class="nt">requests</span><span class="p">:</span>
<span class="w"> </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">100m</span>
<span class="w"> </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">100Mi</span>
<span class="w"> </span><span class="nt">volumeMounts</span><span class="p">:</span>
<span class="w"> </span><span class="c1"># nginx requires some configuration</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">demo-config-map</span>
<span class="w"> </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">/opt/app-root/etc/nginx.d</span>
</code></pre></div>
<p><code>nginx</code> requires a configuration for our static web site. We will store it in a <code>Config Map</code> and
mount it into the container:</p>
<div class="highlight"><pre><span></span><code><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">ConfigMap</span>
<span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">v1</span>
<span class="nt">metadata</span><span class="p">:</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">demo</span>
<span class="w"> </span><span class="nt">labels</span><span class="p">:</span>
<span class="w"> </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">demo</span>
<span class="nt">data</span><span class="p">:</span>
<span class="w"> </span><span class="nt">demo.conf</span><span class="p">:</span><span class="w"> </span><span class="p p-Indicator">|</span>
<span class="w"> </span><span class="no">server {</span>
<span class="w"> </span><span class="no">server_name _;</span>
<span class="w"> </span><span class="no">listen 8081 default_server;</span>
<span class="w"> </span><span class="no">root /opt/app-root/src/;</span>
<span class="w"> </span><span class="no">location / {</span>
<span class="c1"># we put all assets into "artifacts" directory</span>
<span class="c1"># and here we tell nginx where is the root of our</span>
<span class="c1"># static web site</span>
<span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">root /opt/app-root/src/artifacts;</span>
<span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">try_files $uri /index.html;</span>
<span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">}</span>
<span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">}</span>
</code></pre></div>
<h3>Route</h3>
<p>The networking part is small. We need <code>Service</code> and <code>Route</code> manifests after that our static web
site is ready:</p>
<div class="highlight"><pre><span></span><code><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Service</span>
<span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">v1</span>
<span class="nt">metadata</span><span class="p">:</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">demo</span>
<span class="w"> </span><span class="nt">labels</span><span class="p">:</span>
<span class="w"> </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">demo</span>
<span class="nt">spec</span><span class="p">:</span>
<span class="w"> </span><span class="nt">ports</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">TCP</span>
<span class="w"> </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">8081</span>
<span class="w"> </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">8081</span>
<span class="w"> </span><span class="c1"># Don't forget about correct pods selector</span>
<span class="w"> </span><span class="nt">selector</span><span class="p">:</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">demo</span>
</code></pre></div>
<div class="highlight"><pre><span></span><code><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Route</span>
<span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">route.openshift.io/v1</span>
<span class="nt">metadata</span><span class="p">:</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">demo</span>
<span class="w"> </span><span class="nt">labels</span><span class="p">:</span>
<span class="w"> </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">demo</span>
<span class="nt">spec</span><span class="p">:</span>
<span class="w"> </span><span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">demo.example.com</span>
<span class="w"> </span><span class="nt">to</span><span class="p">:</span>
<span class="w"> </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Service</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">demo</span>
<span class="w"> </span><span class="nt">port</span><span class="p">:</span>
<span class="w"> </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">8081</span>
<span class="w"> </span><span class="nt">tls</span><span class="p">:</span>
<span class="w"> </span><span class="nt">termination</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">edge</span>
<span class="w"> </span><span class="nt">insecureEdgeTerminationPolicy</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Redirect</span>
<span class="w"> </span><span class="nt">wildcardPolicy</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">None</span>
</code></pre></div>
<h2>Pipeline</h2>
<p>We have all manifests in place and we applied the to Openshift using <code>oc apply -f</code> command. The next
step is to set up Gitlab CI pipelines that trigger builds and update the deployment. Here is the
content of <code>.gitlab-ci.yml</code> for <code>data-script</code> repository:</p>
<div class="highlight"><pre><span></span><code><span class="nt">stages</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Testing</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Build artifacts</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Build runner</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Rollout runner</span>
<span class="nt">testing</span><span class="p">:</span>
<span class="w"> </span><span class="c1"># tests must pass before building artifacts</span>
<span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">some-registry/your-builder:latest</span>
<span class="w"> </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Testing</span>
<span class="w"> </span><span class="nt">script</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">test.sh</span>
<span class="w"> </span><span class="nt">tags</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">shared</span>
<span class="nt">build-artifacts</span><span class="p">:</span>
<span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">quay.io/openshift/origin-cli:latest</span>
<span class="w"> </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Build artifacts</span>
<span class="w"> </span><span class="nt">script</span><span class="p">:</span>
<span class="w"> </span><span class="c1"># oc utility has a nice feature to show the build logs</span>
<span class="w"> </span><span class="c1"># and return the status code after finishing building</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="p p-Indicator">|</span>
<span class="w"> </span><span class="no">oc --server=<openshift-api-url> \</span>
<span class="w"> </span><span class="no">--namespace=<namespace> \</span>
<span class="w"> </span><span class="no">--token=$PIPELINE_TOKEN \</span>
<span class="w"> </span><span class="no">start-build json-artifacts --wait --follow</span>
<span class="w"> </span><span class="c1"># start-build frontend-artifacts --wait --follow</span>
<span class="w"> </span><span class="c1"># for https://gitlab.example.com/frontend repo</span>
<span class="w"> </span><span class="nt">tags</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">shared</span>
<span class="w"> </span><span class="nt">only</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">tags</span>
<span class="nt">build-runner</span><span class="p">:</span>
<span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">quay.io/openshift/origin-cli:latest</span>
<span class="w"> </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Build runner</span>
<span class="w"> </span><span class="nt">script</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="p p-Indicator">|</span>
<span class="w"> </span><span class="no">oc --server=<openshift-api-url> \</span>
<span class="w"> </span><span class="no">--namespace=<namespace> \</span>
<span class="w"> </span><span class="no">--token=$PIPELINE_TOKEN \</span>
<span class="w"> </span><span class="no">start-build runner --wait --follow</span>
<span class="w"> </span><span class="nt">tags</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">shared</span>
<span class="w"> </span><span class="nt">only</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">tags</span>
<span class="nt">rollout-runner</span><span class="p">:</span>
<span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">quay.io/openshift/origin-cli:latest</span>
<span class="w"> </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Rollout runner</span>
<span class="w"> </span><span class="nt">script</span><span class="p">:</span>
<span class="w"> </span><span class="c1"># in order to upgrade the image</span>
<span class="w"> </span><span class="c1"># we just delete all pods with certain labels</span>
<span class="w"> </span><span class="c1"># replica set automatically spawns a new pod</span>
<span class="w"> </span><span class="c1"># with new image because we set Image Pull Policy:</span>
<span class="w"> </span><span class="c1"># Always</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="p p-Indicator">|</span>
<span class="w"> </span><span class="no">oc --server=<openshift-api-url> \</span>
<span class="w"> </span><span class="no">--namespace=<namespace> \</span>
<span class="w"> </span><span class="no">--token=$PIPELINE_TOKEN \</span>
<span class="w"> </span><span class="no">delete pod -l name=demo</span>
<span class="w"> </span><span class="c1"># waiting until all pods are ready</span>
<span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">oc --server=<openshift-api-url> \</span>
<span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">--namespace=<namespace> \</span>
<span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">--token=$PIPELINE_TOKEN \</span>
<span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">wait --for=condition=Ready pod -l name=demo --timeout=60s</span>
<span class="w"> </span><span class="nt">tags</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">shared</span>
<span class="w"> </span><span class="nt">only</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">tags</span>
</code></pre></div>
<h2>Summary</h2>
<p>Gitlab CI and Openshift offer vast of possibilities in building and deploying. I showed you a case
with two repositories. In my opinion it's not so common. Usually we deal with one repository, for
example it could be <code>sphinx</code> based documentation. Nevertheless I think it's a good show case of
<code>Source</code> build strategy and <code>S2I</code> approach. It demonstrates that we can consume building artifacts
from more than one container image. I would concern about loads of yamls but we cannot do much with
it. This format is standard de-facto in CI and devops world.</p>
<p>References:</p>
<ul>
<li><a href="https://docs.openshift.com/container-platform/4.7/openshift_images/using_images/using-s21-images.html">https://docs.openshift.com/container-platform/4.7/openshift_images/using_images/using-s21-images.html</a></li>
<li><a href="https://docs.openshift.com/container-platform/4.7/cicd/builds/understanding-image-builds.html">https://docs.openshift.com/container-platform/4.7/cicd/builds/understanding-image-builds.html</a></li>
<li><a href="https://docs.gitlab.com/ee/ci/yaml/README.html">https://docs.gitlab.com/ee/ci/yaml/README.html</a></li>
</ul>
<p><a href="https://github.com/quarckster/blog.misharov.pro/discussions/16">Discuss on Github</a></p>How to run a rootless podman service in Github Actions2021-05-16T00:00:00+02:002021-05-16T00:00:00+02:00tag:blog.misharov.pro,2021-05-16:/2021-05-16/systemd-github-actionsManaging user services by systemd in Github Actions runners is not so obvious as in your local
machine. Here is what I found when I was trying to start rootless podman as a service.<p>Managing user services by <code>systemd</code> in Github Actions runners is not so obvious as in your local
machine. Here is what I found when I was trying to start rootless podman as a service.</p>
<h2>Podman socket</h2>
<p>I was playing with <code>podman-py</code> package and for that I needed to start a podman socket. Systems with
<code>systemd</code> use <code>systemctl</code> utility to manage services, sockets, timers and other objects. Systemd can
connect to the user service manager and this is important if you run podman in the rootless mode.
Thus you need this command to start podman service and make it listen to a socket:</p>
<div class="highlight"><pre><span></span><code>systemctl<span class="w"> </span>--user<span class="w"> </span><span class="nb">enable</span><span class="w"> </span>--now<span class="w"> </span>podman.socket
systemctl<span class="w"> </span>--user<span class="w"> </span>status<span class="w"> </span>podman.socket<span class="w"> </span>
●<span class="w"> </span>podman.socket<span class="w"> </span>-<span class="w"> </span>Podman<span class="w"> </span>API<span class="w"> </span>Socket
<span class="w"> </span>Loaded:<span class="w"> </span>loaded<span class="w"> </span><span class="o">(</span>/usr/lib/systemd/user/podman.socket<span class="p">;</span><span class="w"> </span>enabled<span class="p">;</span><span class="w"> </span>vendor<span class="w"> </span>preset:<span class="w"> </span>enabled<span class="o">)</span>
<span class="w"> </span>Active:<span class="w"> </span>active<span class="w"> </span><span class="o">(</span>listening<span class="o">)</span><span class="w"> </span>since<span class="w"> </span>Sat<span class="w"> </span><span class="m">2021</span>-05-15<span class="w"> </span><span class="m">15</span>:17:54<span class="w"> </span>CEST<span class="p">;</span><span class="w"> </span>1h<span class="w"> </span>10min<span class="w"> </span>ago
<span class="w"> </span>Triggers:<span class="w"> </span>●<span class="w"> </span>podman.service
<span class="w"> </span>Docs:<span class="w"> </span>man:podman-system-service<span class="o">(</span><span class="m">1</span><span class="o">)</span>
<span class="w"> </span>Listen:<span class="w"> </span>/run/user/1000/podman/podman.sock<span class="w"> </span><span class="o">(</span>Stream<span class="o">)</span>
<span class="w"> </span>CGroup:<span class="w"> </span>/user.slice/user-1000.slice/user@1000.service/podman.socket
May<span class="w"> </span><span class="m">15</span><span class="w"> </span><span class="m">15</span>:17:54<span class="w"> </span>thinkpad-t480s<span class="w"> </span>systemd<span class="o">[</span><span class="m">959</span><span class="o">]</span>:<span class="w"> </span>Listening<span class="w"> </span>on<span class="w"> </span>Podman<span class="w"> </span>API<span class="w"> </span>Socket
</code></pre></div>
<p>After that you can use podman REST API and <code>podman-py</code>.</p>
<h2>Github Actions runner</h2>
<p>Although this command perfectly works on a regular system in Github Actions I got the error:</p>
<div class="highlight"><pre><span></span><code>Failed to connect to bus: No such file or directory
</code></pre></div>
<p>After some googling I found that the reason is <code>/lib/systemd/systemd --user</code> process doesn't run in
a runner. This process is responsible for managing user services. In order to enable it you have to
enable lingering for a user you logged in:</p>
<div class="highlight"><pre><span></span><code>loginctl<span class="w"> </span>enable-linger<span class="w"> </span><span class="k">$(</span>whoami<span class="k">)</span>
</code></pre></div>
<p>But this is not enough. Rootless podman puts its socket to this path
<code>$XDG_RUNTIME_DIR/podman/podman.sock</code> and Github Actions environment doesn't have <code>$XDG_RUNTIME_DIR</code>
variable set. Let's set it in this way:</p>
<div class="highlight"><pre><span></span><code><span class="nb">export</span><span class="w"> </span><span class="nv">XDG_RUNTIME_DIR</span><span class="o">=</span>/run/user/<span class="nv">$UID</span>
</code></pre></div>
<p>or you can add it to Github Actions environment like this:</p>
<div class="highlight"><pre><span></span><code><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Set XDG_RUNTIME_DIR</span>
<span class="w"> </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">echo "XDG_RUNTIME_DIR=/run/user/$UID" >> $GITHUB_ENV</span>
</code></pre></div>
<p>And there is one more problem. <a href="https://github.com/eriksjolund">@eriksjolund</a> found that
<code>loginctl enable-linger</code> needs some time to finish and <code>sleep 1</code> is needed after.</p>
<h2>Podman service</h2>
<p>Despite on I was able to enable podman socket using systemd facilities the implementation looks a
bit hacky. Fortunately, <code>podman</code> can create a listening service that will answer API calls. I used
this command to start the service in the background that listens to API calls in
<code>${XDG_RUNTIME_DIR}/podman/podman.sock</code>:</p>
<div class="highlight"><pre><span></span><code>podman<span class="w"> </span>system<span class="w"> </span>service<span class="w"> </span>--time<span class="o">=</span><span class="m">0</span><span class="w"> </span>unix://<span class="si">${</span><span class="nv">XDG_RUNTIME_DIR</span><span class="si">}</span>/podman/podman.sock<span class="w"> </span><span class="p">&</span>
</code></pre></div>
<p>References:</p>
<ul>
<li><a href="https://superuser.com/questions/1561076/systemctl-use-failed-to-connect-to-bus-no-such-file-or-directory-debian-9">https://superuser.com/questions/1561076/systemctl-use-failed-to-connect-to-bus-no-such-file-or-directory-debian-9</a></li>
<li><a href="https://github.com/eriksjolund/user-systemd-service-actions-workflow/">https://github.com/eriksjolund/user-systemd-service-actions-workflow/</a></li>
<li><a href="https://lists.podman.io/archives/list/podman@lists.podman.io/thread/E2PHNHZ6QVWMOT4Y7PVRIPDY7SVFTWG2/?sort=thread">https://lists.podman.io/archives/list/podman@lists.podman.io/thread/E2PHNHZ6QVWMOT4Y7PVRIPDY7SVFTWG2/?sort=thread</a></li>
<li><a href="http://docs.podman.io/en/latest/markdown/podman-system-service.1.html">http://docs.podman.io/en/latest/markdown/podman-system-service.1.html</a></li>
<li><a href="https://www.freedesktop.org/software/systemd/man/loginctl.html">https://www.freedesktop.org/software/systemd/man/loginctl.html</a></li>
<li><a href="https://github.com/containers/podman-py">https://github.com/containers/podman-py</a></li>
</ul>
<p><a href="https://github.com/quarckster/blog.misharov.pro/discussions/14">Discuss on Github</a></p>MockServer2021-04-04T00:00:00+02:002021-04-04T00:00:00+02:00tag:blog.misharov.pro,2021-04-04:/2021-04-04/mockserverIn unit testing we often mock various objects, libraries and side effects. For example it can be a
response from some web service. But what should we do in integration and functional tests?<p>In unit testing we often mock various objects, libraries and side effects. For example it can be a
response from some web service. But what should we do in integration and functional tests?</p>
<h2>The problem</h2>
<p>Let's imagine that your application receives data from a third-party web service and you want to
run integration tests. Running tests against the real web service is not the best idea. You probably
need to provide credentials and maybe other sensitive data that doesn't make sense to pass into a
testing environment. A good approach would be set up a fake web server that can emulate behavior
of the real third-party service.</p>
<h2>The solution</h2>
<p>I found an interesting piece of software called <code>MockServer</code>. Let me quote the description from the
official site:</p>
<blockquote>
<p>For any system you integrate with via HTTP or HTTPS MockServer can be used as:</p>
<ul>
<li>a mock configured to return specific responses for different requests</li>
<li>a proxy recording and optionally modifying requests and responses</li>
<li>both a proxy for some requests and a mock for other requests at the same time</li>
</ul>
<p>When MockServer receives a request it matches the request against active expectations that have
been configured, if no matches are found it proxies the request if appropriate otherwise a 404 is
returned.</p>
<p>For each request received the following steps happen:</p>
<ul>
<li>find matching expectation and perform action</li>
<li>if no matching expectation proxy request</li>
<li>if not a proxy request return 404</li>
</ul>
</blockquote>
<p>Sounds pretty cool. Let me share how I used it in one of my pet projects.</p>
<h2>Usage</h2>
<p>In order to start using <code>MockServer</code> you should define which endpoints you would like to mock.
Perhaps you will need to modify the code of your application to have the ability to configure a
proxy server. Then you need to record requests and responses using built-in proxy of <code>MockServer</code>.
Just run the server:</p>
<div class="highlight"><pre><span></span><code>java<span class="w"> </span>-jar<span class="w"> </span>mockserver-netty-5.11.2-jar-with-dependencies.jar<span class="w"> </span>-serverPort<span class="w"> </span><span class="m">8080</span>
</code></pre></div>
<p>And configure system proxy to <code>http://localhost:8080/</code>. Send some requests via proxy and then you
can download recorded requests and responses via <code>MockServer</code> REST API:</p>
<div class="highlight"><pre><span></span><code>curl<span class="w"> </span>-X<span class="w"> </span>PUT<span class="w"> </span><span class="s2">"http://localhost:8080/mockserver/retrieve?type=REQUEST_RESPONSES"</span>
</code></pre></div>
<p>The output is just a json with the following structure:</p>
<div class="highlight"><pre><span></span><code><span class="p">[</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">"httpRequest"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">"method"</span><span class="p">:</span><span class="w"> </span><span class="s2">"GET"</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/some/endpoint"</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nt">"httpResponse"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">"statusCode"</span><span class="p">:</span><span class="w"> </span><span class="mi">200</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"reasonPhrase"</span><span class="p">:</span><span class="w"> </span><span class="s2">"OK"</span><span class="p">,</span>
<span class="w"> </span><span class="nt">"body"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="mi">200</span><span class="p">,</span>
<span class="w"> </span><span class="err">...</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="err">...</span>
<span class="p">]</span>
</code></pre></div>
<p>The replay contains a lot of request and responses aspects such as cookies, various headers and so
on. You might not need to store all of them therefore feel free to edit the file to keep only
required data. After that you can use this file in your tests. Run the <code>MockServer</code> and specify
the path to the replay file:</p>
<div class="highlight"><pre><span></span><code><span class="nb">export</span><span class="w"> </span><span class="nv">MOCKSERVER_INITIALIZATION_JSON_PATH</span><span class="o">=</span><path<span class="w"> </span>to<span class="w"> </span>replay<span class="w"> </span>file>
java<span class="w"> </span>-jar<span class="w"> </span>mockserver-netty-5.11.2-jar-with-dependencies.jar<span class="w"> </span>-serverPort<span class="w"> </span><span class="m">8080</span>
</code></pre></div>
<p>When your application sends a request to the third-party web service via <code>MockServer</code> proxy it will
get exactly the same responses that you specified in <code>httpResponse</code> stanzas.</p>
<h3>HTTPS & TLS</h3>
<p>Nowadays almost all web applications work only wih HTTPS. It means that you cannot intercept traffic
between the client and the server. <code>MockServer</code> is able to mock the behaviour of multiple hostnames
and present a valid X.509 Certificates for them. <code>MockServer</code> achieves this by dynamically
generating its X.509 Certificate using an in-memory list of hostnames and ip addresses. Create a
<code>mockserver.properties</code> file with the following content:</p>
<div class="highlight"><pre><span></span><code>###############################
# MockServer & Proxy Settings #
###############################
# Certificate Generation
# dynamically generated CA key pair (if they don't already exist in specified directory)
mockserver.dynamicallyCreateCertificateAuthorityCertificate=true
# save dynamically generated CA key pair in working directory
mockserver.directoryToSaveDynamicSSLCertificate=.
# certificate domain name (default "localhost")
mockserver.sslCertificateDomainName=localhost
# comma separated list of ip addresses for Subject Alternative Name domain names (default empty list)
mockserver.sslSubjectAlternativeNameDomains=<third-party web service host name>
</code></pre></div>
<p>Run the <code>MockServer</code>:</p>
<div class="highlight"><pre><span></span><code><span class="nb">export</span><span class="w"> </span><span class="nv">MOCKSERVER_PROPERTY_FILE</span><span class="o">=</span>mockserver.properties
java<span class="w"> </span>-jar<span class="w"> </span>mockserver-netty-5.11.2-jar-with-dependencies.jar<span class="w"> </span>-serverPort<span class="w"> </span><span class="m">8080</span>
</code></pre></div>
<p>And after that you will find two new files: <code>CertificateAuthorityCertificate.pem</code> and
<code>PKCS8CertificateAuthorityPrivateKey.pem</code>. We should add MockServer's CA into the system list of
trusted Certificate Authorities. The following commands are applicable for Ubuntu:</p>
<div class="highlight"><pre><span></span><code>sudo<span class="w"> </span>cp<span class="w"> </span>CertificateAuthorityCertificate.pem<span class="w"> </span>/usr/local/share/ca-certificates/MockServer.crt
sudo<span class="w"> </span>update-ca-certificates
</code></pre></div>
<p>WARNING! Remember if you add a new Certificate Authority into your system the issuer can listen to
your traffic. Do not install random CAs from the internet!</p>
<p>In case of <code>MockServer</code> it's safe because you are the issuer :).</p>
<p>After these procedures <code>MockServer</code> will be able to record requests and responses to any HTTPS
resource.</p>
<h2>Conclusion</h2>
<p><code>MockServer</code> is a powerful tool that gives unlimited ability for creating fake APIs of any web
service. I described a small amount of its features and abilities because <code>MockServer</code> really can do
a lot. More details you will find in very good documentation on the official site.</p>
<p>References:</p>
<ul>
<li><a href="https://mock-server.com/">https://mock-server.com/</a></li>
</ul>
<p><a href="https://github.com/quarckster/blog.misharov.pro/discussions/12">Discuss on Github</a></p>Kodi add-on testing2021-03-03T00:00:00+01:002021-03-03T00:00:00+01:00tag:blog.misharov.pro,2021-03-03:/2021-03-03/kodi-addon-testingOk, you develop a Kodi add-on and you would like to have CI for your code. What options do you have?
How Kodi add-ons can be tested? And how to configure a CI for a Kodi add-on code? Let me share my
experience.<p>Ok, you develop a Kodi add-on and you would like to have CI for your code. What options do you have?
How Kodi add-ons can be tested? And how to configure a CI for a Kodi add-on code? Let me share my
experience.</p>
<h2>Unit tests</h2>
<p>According to the testing pyramid the bulk of your tests are unit tests:</p>
<p><img class="image-center" alt="diagram 1" src="https://blog.misharov.pro/assets/img/2021-03-03-kodi-addon-testing-1.png"/></p>
<p>The problem with Kodi is its python API is accessible only within Kodi runtime. For example, if you
need to get the addon info you can use the following code:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">xbmcaddon</span>
<span class="n">xbmcaddon</span><span class="o">.</span><span class="n">Addon</span><span class="p">()</span><span class="o">.</span><span class="n">getAddonInfo</span><span class="p">(</span><span class="s2">"version"</span><span class="p">)</span>
</code></pre></div>
<p><code>xbmcaddon</code> is a python interface for Kodi C++ code and it cannot be installed in your virtual
environment. You have to mock it. Combining python mocking library <code>unittest.mock</code> and
<a href="https://github.com/RomanVM">@RomanVM</a>'s Kodi stubs you can create some unit tests. But I must
admit that it might be not so trivial to write them. Your tests will be as good as your mocks are.
You might come to the situation when test maintenance takes more time than working on the add-on's
code. In my opinion, it makes sense to write unit tests for the code that doesn't use Kodi python
API. It might be various helpers, utilities, or really simple functions and classes that don't
require to mock a lot of side effects.
Of course, I highly recommend using <code>pytest</code> as a testing library. It's really the best tool for
test development. Moreover, it doesn't matter where your tests are located in the testing pyramid.</p>
<h2>Integration tests</h2>
<p>Integration tests assume that your code will be tested in the running Kodi instance. Now we need to
solve the following problems to write the automation:</p>
<ul>
<li>we need a running Kodi and we should be able to specify its version;</li>
<li>we should be able to install a development version of the testing add-on;</li>
<li>we need to have an interface to interact with Kodi automatically;</li>
<li>we should be able to check expected results.</li>
</ul>
<h3>Kodi setup</h3>
<p>I have a strong opinion that containers ideally fit this task and other similar tasks such as
UI testing of web applications. In my previous post <a href="https://blog.misharov.pro/2021-02-14/conkodi">Conkodi</a>
I explained how to create and run a container with Kodi. Using this container image we can configure
our setup stage in pytest's <code>conftest.py</code>:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">subprocess</span>
<span class="kn">import</span> <span class="nn">pytest</span>
<span class="c1"># it's just a helping function for podman CLI</span>
<span class="k">def</span> <span class="nf">podman</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">):</span>
<span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">([</span><span class="s2">"podman"</span><span class="p">]</span> <span class="o">+</span> <span class="nb">list</span><span class="p">(</span><span class="n">args</span><span class="p">),</span> <span class="n">stdout</span><span class="o">=</span><span class="n">subprocess</span><span class="o">.</span><span class="n">DEVNULL</span><span class="p">)</span>
<span class="c1"># we want to have running Kodi container during whole testing session</span>
<span class="nd">@pytest</span><span class="o">.</span><span class="n">fixture</span><span class="p">(</span><span class="n">scope</span><span class="o">=</span><span class="s2">"session"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">run_kodi</span><span class="p">():</span>
<span class="c1"># remove containers from previous run </span>
<span class="n">podman</span><span class="p">(</span><span class="s2">"rm"</span><span class="p">,</span> <span class="s2">"-f"</span><span class="p">,</span> <span class="s2">"kodi"</span><span class="p">)</span>
<span class="n">podman</span><span class="p">(</span>
<span class="s2">"run"</span><span class="p">,</span>
<span class="s2">"--detach"</span><span class="p">,</span> <span class="c1"># run a container in the background</span>
<span class="s2">"--name=kodi"</span><span class="p">,</span> <span class="c1"># with the name "kodi"</span>
<span class="c1"># make various ports accessible for the host</span>
<span class="s2">"--publish=8080:8080"</span><span class="p">,</span> <span class="c1"># Kodi JSON RPC listens to this port</span>
<span class="s2">"--publish=5999:5999"</span><span class="p">,</span> <span class="c1"># this port needs accessing VNC server of the container</span>
<span class="s2">"--publish=9777:9777/udp"</span><span class="p">,</span> <span class="c1"># Kodi EventServer listens to this port</span>
<span class="s2">"quay.io/quarck/conkodi:19"</span><span class="p">,</span> <span class="c1"># the name and the tag of the container image with Kodi</span>
<span class="p">)</span>
<span class="k">yield</span>
<span class="n">podman</span><span class="p">(</span><span class="s2">"stop"</span><span class="p">,</span> <span class="s2">"kodi"</span><span class="p">)</span> <span class="c1"># teardown, stop the container</span>
</code></pre></div>
<h3>Add-on installation</h3>
<p>Kodi add-ons can be installed only via the UI. You need to find a zip archive in the filesystem and
click several buttons in Kodi. There is no CLI or API that allows the installation of an add-on.
What can we do here? First of all, let's make a small study of what happens when you install a Kodi
add-on. After the installation, the add-on archive is unpacked into <code>addons</code> directory. Besides, new
entries are created in the databases <code>Addons33.db</code> and <code>Textures13.db</code>. Knowing these facts we can
setup our container in such a way that Kodi will have the add-on installed. We can mount host
directories with the code of the add-on and the prepared database in a Kodi container. Let's update
the code from the previous paragraph:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">shutil</span>
<span class="kn">import</span> <span class="nn">subprocess</span>
<span class="kn">import</span> <span class="nn">pytest</span>
<span class="k">def</span> <span class="nf">podman</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">):</span>
<span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">([</span><span class="s2">"podman"</span><span class="p">]</span> <span class="o">+</span> <span class="nb">list</span><span class="p">(</span><span class="n">args</span><span class="p">),</span> <span class="n">stdout</span><span class="o">=</span><span class="n">subprocess</span><span class="o">.</span><span class="n">DEVNULL</span><span class="p">)</span>
<span class="nd">@pytest</span><span class="o">.</span><span class="n">fixture</span><span class="p">(</span><span class="n">scope</span><span class="o">=</span><span class="s2">"session"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">build_plugin</span><span class="p">():</span>
<span class="c1"># Here we copy a development version of the add-on into the directory with where all Kodi</span>
<span class="c1"># add-ons are stored</span>
<span class="n">shutil</span><span class="o">.</span><span class="n">copytree</span><span class="p">(</span><span class="s2">"/dir/with/addon/code"</span><span class="p">,</span> <span class="s2">"/dir/with/addons/some.addon"</span><span class="p">)</span>
<span class="k">yield</span>
<span class="n">shutil</span><span class="o">.</span><span class="n">rmtree</span><span class="p">(</span><span class="s2">"/dir/with/addons/some.addon"</span><span class="p">)</span>
<span class="nd">@pytest</span><span class="o">.</span><span class="n">fixture</span><span class="p">(</span><span class="n">scope</span><span class="o">=</span><span class="s2">"session"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">run_kodi_container</span><span class="p">(</span><span class="n">build_plugin</span><span class="p">):</span>
<span class="n">podman</span><span class="p">(</span><span class="s2">"rm"</span><span class="p">,</span> <span class="s2">"-f"</span><span class="p">,</span> <span class="s2">"kodi"</span><span class="p">)</span>
<span class="n">podman</span><span class="p">(</span>
<span class="s2">"run"</span><span class="p">,</span>
<span class="s2">"--detach"</span><span class="p">,</span>
<span class="s2">"--name=kodi"</span><span class="p">,</span>
<span class="s2">"--publish=8080:8080"</span><span class="p">,</span>
<span class="s2">"--publish=5999:5999"</span><span class="p">,</span>
<span class="s2">"--publish=9777:9777/udp"</span><span class="p">,</span>
<span class="c1"># Mounting a directory with a development version of the add-on </span>
<span class="s2">"--volume=/some/host/dir/addons/:/home/kodi/.kodi/addons"</span><span class="p">,</span>
<span class="c1"># Mounting a directory with prepared databases</span>
<span class="s2">"--volume=/some/host/dir/Database/:/home/kodi/.kodi/userdata/Database"</span><span class="p">,</span>
<span class="s2">"quay.io/quarck/conkodi:19"</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">yield</span>
<span class="n">podman</span><span class="p">(</span><span class="s2">"pod"</span><span class="p">,</span> <span class="s2">"stop"</span><span class="p">,</span> <span class="s2">"kodi"</span><span class="p">)</span>
</code></pre></div>
<h3>Kodi JSON RPC</h3>
<p>Ok, we have a running container with Kodi and installed add-on and now it would be nice to have an
API for making various actions in Kodi. And there is some! Kodi provides JSON RPC API that allows
you to get a list of the items of a directory, execute addons with parameters, get properties of GUI
windows, and so on and so on. There are several python clients and I stopped my choice on
<code>kodi-json</code>. Virtually it supports all available methods. Let me demonstrate how it can be used:</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">kodijson</span> <span class="kn">import</span> <span class="n">Kodi</span>
<span class="c1"># Instantiate a Kodi object</span>
<span class="n">kodi</span> <span class="o">=</span> <span class="n">Kodi</span><span class="p">(</span><span class="s2">"http://127.0.0.1:8080"</span><span class="p">)</span>
<span class="c1"># Get list of items of the root directory of some addon</span>
<span class="n">kodi</span><span class="o">.</span><span class="n">Files</span><span class="o">.</span><span class="n">GetDirectory</span><span class="p">(</span><span class="n">directory</span><span class="o">=</span><span class="s2">"plugin://some.addon"</span><span class="p">)</span>
</code></pre></div>
<p>We could even create a fixture in our test suite:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">pytest</span>
<span class="kn">from</span> <span class="nn">kodijson</span> <span class="kn">import</span> <span class="n">Kodi</span>
<span class="nd">@pytest</span><span class="o">.</span><span class="n">fixture</span><span class="p">(</span><span class="n">scope</span><span class="o">=</span><span class="s2">"session"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">kodi</span><span class="p">(</span><span class="n">run_kodi_container</span><span class="p">):</span>
<span class="k">return</span> <span class="n">Kodi</span><span class="p">(</span><span class="n">JSON_RPC_URL</span><span class="p">)</span>
</code></pre></div>
<p>And then use it in tests:</p>
<div class="highlight"><pre><span></span><code><span class="n">EXPECTED_ROOT_DIR</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">{</span>
<span class="s2">"file"</span><span class="p">:</span> <span class="s2">"plugin://some.addon/some_item/"</span><span class="p">,</span>
<span class="s2">"filetype"</span><span class="p">:</span> <span class="s2">"file"</span><span class="p">,</span>
<span class="s2">"label"</span><span class="p">:</span> <span class="s2">"Some item"</span><span class="p">,</span>
<span class="s2">"type"</span><span class="p">:</span> <span class="s2">"unknown"</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="s2">"file"</span><span class="p">:</span> <span class="s2">"plugin://some.addon/some_dir/"</span><span class="p">,</span>
<span class="s2">"filetype"</span><span class="p">:</span> <span class="s2">"directory"</span><span class="p">,</span>
<span class="s2">"label"</span><span class="p">:</span> <span class="s2">"Some dir"</span><span class="p">,</span>
<span class="s2">"type"</span><span class="p">:</span> <span class="s2">"unknown"</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">]</span>
<span class="k">def</span> <span class="nf">test_root_dir</span><span class="p">(</span><span class="n">kodi</span><span class="p">):</span>
<span class="n">response</span> <span class="o">=</span> <span class="n">kodi</span><span class="o">.</span><span class="n">Files</span><span class="o">.</span><span class="n">GetDirectory</span><span class="p">(</span><span class="n">directory</span><span class="o">=</span><span class="s2">"plugin://some.addon"</span><span class="p">)</span>
<span class="k">assert</span> <span class="n">ROOT_DIR</span> <span class="o">==</span> <span class="n">resp</span><span class="p">[</span><span class="s2">"result"</span><span class="p">][</span><span class="s2">"files"</span><span class="p">]</span>
</code></pre></div>
<p>That's the basics of the integration tests of a Kodi add-on. Despite JSON RPC doesn't allow
you to do everything you can do via GUI in most cases it will be enough. You can even send keyboard
button events and navigate through the UI but it might be error-prone.</p>
<p>References:</p>
<ul>
<li><a href="https://kodi.tv">https://kodi.tv</a></li>
<li><a href="https://kodi.wiki/view/JSON-RPC_API/v12">https://kodi.wiki/view/JSON-RPC_API/v12</a></li>
<li><a href="https://github.com/jcsaaddupuy/python-kodijson">https://github.com/jcsaaddupuy/python-kodijson</a></li>
<li><a href="https://github.com/quarckster/conkodi">https://github.com/quarckster/conkodi</a></li>
<li><a href="https://quay.io/repository/quarck/conkodi?tab=tags">https://quay.io/repository/quarck/conkodi?tab=tags</a></li>
<li><a href="https://podman.io">https://podman.io</a></li>
</ul>
<p><a href="https://github.com/quarckster/blog.misharov.pro/discussions/11">Discuss on Github</a></p>Kodi container image2021-02-14T00:00:00+01:002021-02-14T00:00:00+01:00tag:blog.misharov.pro,2021-02-14:/2021-02-14/conkodiDuring add-on development I use Kodi installed on my operating system for testing. But what if I
need to test my changes on various Kodi versions? It's not so easy to install different versions
of a package on such distros as Ubuntu. We could try some Kodi container image but I decided to
build my own.<p>During add-on development I use Kodi installed on my operating system for testing. But what if I
need to test my changes on various Kodi versions? It's not so easy to install different versions
of a package on such distros as Ubuntu. We could try some Kodi container image but I decided to
build my own.</p>
<h2>Existing Kodi container images</h2>
<p>Before I started working on my Kodi container image I checked what other people did in this area.</p>
<h3><a href="https://github.com/linuxserver/docker-kodi-headless">docker.io/linuxserver/kodi-headless</a></h3>
<p>This image has patched Kodi in order to run it without graphical user interface. The README has
this description "most useful for a mysql setup of kodi to allow library updates to be sent without
the need for a player system to be permanently on". Despite on its advantages this image doesn't
fully fits to my needs. If you develop an add-on with UI you would also like see how the elements
are displayed in Kodi.</p>
<h3><a href="https://github.com/ehough/docker-kodi">docker.io/erichough/kodi</a></h3>
<p>This one has completely different purpose. The goal is to provide containerized Kodi with full
support of audio and video. It requires <code>x11docker</code> to be installed on the host in order to forward
streams from the container to the host. <code>docker.io/erichough/kodi</code> doesn't fit my needs as well. I
don't need support of audio and video playback and I don't want to install any dependencies other
than docker or podman.</p>
<p>I think these two images are the most popular in the Docker Hub and they either don't missing GUI
or have redundant dependencies. That's why I decided to create my own Kodi container image.</p>
<h2>Conkodi</h2>
<p>I need a minimalistic container image with Kodi that is able to display GUI without audio and
video playback capabilities. After several attempts I came to this dockerfile:</p>
<div class="highlight"><pre><span></span><code><span class="c"># Team Kodi officially supports ppa for Ubuntu</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">ubuntu:20.04</span>
<span class="c"># Packages installation should be unattended</span>
<span class="k">ARG</span><span class="w"> </span><span class="nv">DEBIAN_FRONTEND</span><span class="o">=</span>noninteractive
<span class="c"># YOu can specify a version of Kodi to install as build arg</span>
<span class="k">ARG</span><span class="w"> </span><span class="nv">KODI_VERSION</span><span class="o">=</span><span class="m">18</span>.9
<span class="c"># This needed for running VNC</span>
<span class="k">ENV</span><span class="w"> </span><span class="nv">DISPLAY</span><span class="o">=</span>:99<span class="w"> </span>
<span class="c"># Adding ppa and install dependencies</span>
<span class="k">RUN</span><span class="w"> </span>apt<span class="w"> </span>update<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>apt<span class="w"> </span>install<span class="w"> </span>-y<span class="w"> </span>--no-install-recommends<span class="w"> </span>software-properties-common<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>add-apt-repository<span class="w"> </span>-y<span class="w"> </span>ppa:team-xbmc/ppa<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>apt<span class="w"> </span>-y<span class="w"> </span>purge<span class="w"> </span>openssl<span class="w"> </span>software-properties-common<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>apt<span class="w"> </span>install<span class="w"> </span>-y<span class="w"> </span>--no-install-recommends<span class="w"> </span>dumb-init<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>#<span class="w"> </span>Without<span class="w"> </span>pulseaudio<span class="w"> </span>Kodi<span class="w"> </span>logs<span class="w"> </span>are<span class="w"> </span>unreadble<span class="w"> </span>and<span class="w"> </span>useless
<span class="w"> </span>pulseaudio<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>#<span class="w"> </span>This<span class="w"> </span>needs<span class="w"> </span><span class="k">for</span><span class="w"> </span>establishing<span class="w"> </span>SSL<span class="w"> </span>connections<span class="w"> </span>
<span class="w"> </span>ca-certificates<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>#<span class="w"> </span>Tigervnc<span class="w"> </span>implements<span class="w"> </span>an<span class="w"> </span>X<span class="w"> </span>server<span class="w"> </span>and<span class="w"> </span>provides<span class="w"> </span>VNC<span class="w"> </span>access
<span class="w"> </span>tigervnc-standalone-server<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>tigervnc-xorg-extension<span class="w"> </span><span class="se">\</span>
<span class="w"> </span><span class="nv">kodi</span><span class="o">=</span><span class="m">2</span>:<span class="si">${</span><span class="nv">KODI_VERSION</span><span class="si">}</span>+*<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>apt<span class="w"> </span>-y<span class="w"> </span>--purge<span class="w"> </span>autoremove
<span class="k">COPY</span><span class="w"> </span>start.sh<span class="w"> </span>/
<span class="k">COPY</span><span class="w"> </span>guisettings.xml<span class="w"> </span>/home/kodi/.kodi/userdata/guisettings.xml
<span class="c"># Various permission tweaks</span>
<span class="k">RUN</span><span class="w"> </span>chmod<span class="w"> </span>+x<span class="w"> </span>/start.sh<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>touch<span class="w"> </span>/home/kodi/.Xauthority<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>chgrp<span class="w"> </span>-R<span class="w"> </span><span class="m">0</span><span class="w"> </span>/home/kodi/<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>chmod<span class="w"> </span>-R<span class="w"> </span><span class="nv">g</span><span class="o">=</span>u<span class="w"> </span>/home/kodi/
<span class="k">WORKDIR</span><span class="w"> </span><span class="s">/home/kodi</span>
<span class="c"># VNC port</span>
<span class="k">EXPOSE</span><span class="w"> </span><span class="s">5999</span>
<span class="c"># HTTP port</span>
<span class="k">EXPOSE</span><span class="w"> </span><span class="s">8080</span>
<span class="c"># EventServer port</span>
<span class="k">EXPOSE</span><span class="w"> </span><span class="s">9777/udp</span>
<span class="c"># It always a good practice to run applications in the container under some user</span>
<span class="k">USER</span><span class="w"> </span><span class="s">1001</span>
<span class="k">ENTRYPOINT</span><span class="w"> </span><span class="p">[</span><span class="s2">"/usr/bin/dumb-init"</span><span class="p">,</span><span class="w"> </span><span class="s2">"--"</span><span class="p">]</span>
<span class="k">CMD</span><span class="w"> </span><span class="p">[</span><span class="s2">"/start.sh"</span><span class="p">]</span>
</code></pre></div>
<p>And here the starting script:</p>
<div class="highlight"><pre><span></span><code><span class="ch">#!/bin/sh</span>
pulseaudio<span class="w"> </span>><span class="w"> </span>/dev/null<span class="w"> </span><span class="m">2</span>><span class="p">&</span><span class="m">1</span><span class="w"> </span><span class="p">&</span>
vncserver<span class="w"> </span><span class="nv">$DISPLAY</span><span class="w"> </span>-noxstartup<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-securitytypes<span class="w"> </span>none<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-geometry<span class="w"> </span>1600x900<span class="w"> </span><span class="se">\</span>
<span class="w"> </span><span class="c1"># kodi doesn't start on lower depth values</span>
<span class="w"> </span>-depth<span class="w"> </span><span class="m">24</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-alwaysshared<span class="w"> </span>><span class="w"> </span>/dev/null<span class="w"> </span><span class="m">2</span>><span class="p">&</span><span class="m">1</span><span class="w"> </span><span class="p">&</span>
/usr/lib/x86_64-linux-gnu/kodi/kodi.bin<span class="w"> </span>--standalone<span class="w"> </span>><span class="w"> </span>/dev/null<span class="w"> </span><span class="m">2</span>><span class="p">&</span><span class="m">1</span><span class="w"> </span><span class="p">&</span>
<span class="c1"># Kodi starts with some delay</span>
<span class="k">while</span><span class="w"> </span><span class="o">[</span><span class="w"> </span>!<span class="w"> </span>-f<span class="w"> </span><span class="s2">".kodi/temp/kodi.log"</span><span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span>sleep<span class="w"> </span><span class="m">0</span>.1
<span class="k">done</span>
<span class="c1"># Show kodi.log to the container's stdout</span>
tail<span class="w"> </span>-f<span class="w"> </span>.kodi/temp/kodi.log
</code></pre></div>
<h2>Troubles</h2>
<h3>Bit depth</h3>
<p>Let me share the troubles I faced during development of Conkodi. In the beginning Kodi wasn't able
to start. It crashed immediately with the error message <code>Failed to find matching visual</code>. Here I
should say that I started the vnc server in this way:</p>
<div class="highlight"><pre><span></span><code>vncserver<span class="w"> </span><span class="nv">$DISPLAY</span><span class="w"> </span>-noxstartup<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-securitytypes<span class="w"> </span>none<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-geometry<span class="w"> </span>1600x900<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-depth<span class="w"> </span><span class="m">16</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-alwaysshared<span class="w"> </span>><span class="w"> </span>/dev/null<span class="w"> </span><span class="m">2</span>><span class="p">&</span><span class="m">1</span><span class="w"> </span><span class="p">&</span>
</code></pre></div>
<p>I usually use <code>-depth 16</code> in order to reduce resources consumption but apparently Kodi just doesn't
start if the bit depth lower 24. I had to look into Kodi sources to find root cause of the crash.
I searched the string <code>Failed to find matching visual</code> and found one mention <a href="https://github.com/xbmc/xbmc/blob/5230b683323ca58c62459a371c1306a6cb4d4644/xbmc/windowing/X11/WinSystemX11.cpp#L720">here</a>. I found
<code>GetVisual()</code> method in <code>xbmc/windowing/X11/WinSystemX11GLContext.cpp</code>:</p>
<div class="highlight"><pre><span></span><code><span class="n">XVisualInfo</span><span class="o">*</span><span class="w"> </span><span class="nf">CWinSystemX11GLContext::GetVisual</span><span class="p">()</span>
<span class="p">{</span>
<span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="n">count</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span>
<span class="w"> </span><span class="n">XVisualInfo</span><span class="w"> </span><span class="n">vTemplate</span><span class="p">;</span>
<span class="w"> </span><span class="n">XVisualInfo</span><span class="w"> </span><span class="o">*</span><span class="n">visual</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">nullptr</span><span class="p">;</span>
<span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="n">vMask</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">VisualScreenMask</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">VisualDepthMask</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">VisualClassMask</span><span class="p">;</span>
<span class="w"> </span><span class="n">vTemplate</span><span class="p">.</span><span class="n">screen</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">m_screen</span><span class="p">;</span>
<span class="w"> </span><span class="n">vTemplate</span><span class="p">.</span><span class="n">depth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">24</span><span class="p">;</span>
<span class="w"> </span><span class="n">vTemplate</span><span class="p">.</span><span class="n">c_class</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">TrueColor</span><span class="p">;</span>
<span class="w"> </span><span class="n">visual</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">XGetVisualInfo</span><span class="p">(</span><span class="n">m_dpy</span><span class="p">,</span><span class="w"> </span><span class="n">vMask</span><span class="p">,</span><span class="w"> </span><span class="o">&</span><span class="n">vTemplate</span><span class="p">,</span><span class="w"> </span><span class="o">&</span><span class="n">count</span><span class="p">);</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="n">visual</span><span class="p">)</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="n">vTemplate</span><span class="p">.</span><span class="n">depth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">30</span><span class="p">;</span>
<span class="w"> </span><span class="n">visual</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">XGetVisualInfo</span><span class="p">(</span><span class="n">m_dpy</span><span class="p">,</span><span class="w"> </span><span class="n">vMask</span><span class="p">,</span><span class="w"> </span><span class="o">&</span><span class="n">vTemplate</span><span class="p">,</span><span class="w"> </span><span class="o">&</span><span class="n">count</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="n">visual</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>Aha, perhaps <code>vTemplate.depth = 24;</code> means that Kodi expects that the bit depth equals 24. I changed
the argument for <code>vncserver</code> and Kodi started.</p>
<h3>Kodi logs</h3>
<p>Another problem was Kodi logs. After starting the only thing I could see in the logs was:</p>
<div class="highlight"><pre><span></span><code>ERROR: CActiveAESink::OpenSink - no sink was returned
</code></pre></div>
<p>I wanted to have as minimal image as possible and I didn't include <code>pulseaudio</code> that caused this
error. Having readable Kodi logs was one of my requirements therefore I added pulseaudio as well.</p>
<h3>Volume mounts permissions</h3>
<p>Last problem was to set the correct permission for mounted volumes. I found that the container runs
with <code>umask</code> <code>0022</code> it means new files are created with mode <code>644</code> and directories are created with
<code>755</code>. This permissions don't allow to delete files under host user in mounted directories. I
needed to increase the privileges with <code>sudo</code>. Fortunately, <code>podman</code> has a cool feature to run
containers with a certain umask value:</p>
<div class="highlight"><pre><span></span><code>podman<span class="w"> </span>run<span class="w"> </span>-it<span class="w"> </span>--umask<span class="o">=</span><span class="m">0002</span><span class="w"> </span>--volume<span class="o">=</span>/some/dir:/mnt<span class="w"> </span>quay.io/quarck/conkodi:18
</code></pre></div>
<h2>Building</h2>
<p>If you want build the image locally just clone the repo and use your favorite container engine tool.
I prefer <code>podman</code> and <code>buildah</code>:</p>
<div class="highlight"><pre><span></span><code>git<span class="w"> </span>clone<span class="w"> </span>https://github.com/quarckster/conkodi.git
buildah<span class="w"> </span>bud<span class="w"> </span>--build-arg<span class="o">=</span><version<span class="w"> </span>of<span class="w"> </span>Kodi><span class="w"> </span>-t<span class="w"> </span>conkodi:<version<span class="w"> </span>of<span class="w"> </span>Kodi><span class="w"> </span>-f<span class="w"> </span>conkodi/stable.Dockerfile<span class="w"> </span>conkodi
</code></pre></div>
<p>You can also build an image with nightly version of Kodi:</p>
<div class="highlight"><pre><span></span><code>buildah<span class="w"> </span>bud<span class="w"> </span>-t<span class="w"> </span>conkodi<span class="w"> </span>-f<span class="w"> </span>conkodi/nightly.Dockerfile<span class="w"> </span>conkodi
</code></pre></div>
<h3>Prebuilt images</h3>
<p>I have prebuilt some images and pushed them quay.io registry. You can find list of tags in
<a href="https://quay.io/repository/quarck/conkodi?tab=tags">https://quay.io/repository/quarck/conkodi?tab=tags</a></p>
<h2>Usage</h2>
<p>Just use the following command:</p>
<div class="highlight"><pre><span></span><code>podman<span class="w"> </span>run<span class="w"> </span>-it<span class="w"> </span>--name<span class="w"> </span>kodi<span class="w"> </span>--rm<span class="w"> </span>-p<span class="w"> </span><span class="m">5999</span>:5999<span class="w"> </span>-p<span class="w"> </span><span class="m">8080</span>:8080<span class="w"> </span>-p<span class="w"> </span><span class="m">9777</span>:9777/udp<span class="w"> </span>quay.io/quarck/conkodi:18
</code></pre></div>
<p>You can access Kodi GUI using any VNC client, e.g.:</p>
<div class="highlight"><pre><span></span><code>krdc<span class="w"> </span>vnc://127.0.0.1:5999
</code></pre></div>
<p>References:</p>
<ul>
<li><a href="https://kodi.tv">https://kodi.tv</a></li>
<li><a href="https://github.com/quarckster/conkodi">https://github.com/quarckster/conkodi</a></li>
<li><a href="https://quay.io/repository/quarck/conkodi?tab=tags">https://quay.io/repository/quarck/conkodi?tab=tags</a></li>
<li><a href="https://podman.io">https://podman.io</a></li>
<li><a href="https://buildah.io">https://buildah.io</a></li>
</ul>
<p><a href="https://github.com/quarckster/blog.misharov.pro/discussions/9">Discuss on Github</a></p>install_requires vs requirements.txt2021-01-30T00:00:00+01:002021-01-30T00:00:00+01:00tag:blog.misharov.pro,2021-01-30:/2021-01-30/setup-vs-requirementsWhen I started working at Red Hat my first job was contribution to integration tests of one web
application. The tests are written Python and the repository has both setup.py and
requirements.txt. But why?<p>When I started working at Red Hat my first job was contribution to integration tests of one web
application. The tests are written Python and the repository has both <code>setup.py</code> and
<code>requirements.txt</code>. But why?</p>
<h2>requirements.txt</h2>
<p>Python and many other interpreted languages such as Ruby and JS require a prepared environment to
run some code. This environment should have all dependencies installed with correct versions. In
Python we use <code>requirements.txt</code> file for describing such environment. It's just a text file with
package names and versions. e.g.:</p>
<div class="highlight"><pre><span></span><code>pytest==6.2.2
django==3.1.5
</code></pre></div>
<p>Then using <code>pip</code> we can recreate this environment in any other place. It makes a lot of sense when
we deploy a Python application.</p>
<h2>install_requires</h2>
<p>But what about dependencies of dependencies? Python code is distributed via <em>packages</em>. A Python
package is a directory that follows a certain file structure and has <code>setup.py</code> file that is a
build script for <code>setuptools</code>. If your package uses other packages you can specify them in
<code>install_requires</code> argument:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">setuptools</span>
<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s2">"README.md"</span><span class="p">,</span> <span class="s2">"r"</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">"utf-8"</span><span class="p">)</span> <span class="k">as</span> <span class="n">fh</span><span class="p">:</span>
<span class="n">long_description</span> <span class="o">=</span> <span class="n">fh</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
<span class="n">setuptools</span><span class="o">.</span><span class="n">setup</span><span class="p">(</span>
<span class="n">name</span><span class="o">=</span><span class="s2">"example-pkg-YOUR-USERNAME-HERE"</span><span class="p">,</span> <span class="c1"># Replace with your own username</span>
<span class="n">version</span><span class="o">=</span><span class="s2">"0.0.1"</span><span class="p">,</span>
<span class="n">author</span><span class="o">=</span><span class="s2">"Example Author"</span><span class="p">,</span>
<span class="n">author_email</span><span class="o">=</span><span class="s2">"author@example.com"</span><span class="p">,</span>
<span class="n">description</span><span class="o">=</span><span class="s2">"A small example package"</span><span class="p">,</span>
<span class="n">long_description</span><span class="o">=</span><span class="n">long_description</span><span class="p">,</span>
<span class="n">long_description_content_type</span><span class="o">=</span><span class="s2">"text/markdown"</span><span class="p">,</span>
<span class="n">url</span><span class="o">=</span><span class="s2">"https://github.com/pypa/sampleproject"</span><span class="p">,</span>
<span class="n">install_requires</span><span class="o">=</span><span class="p">[</span><span class="s2">"pytest>6.0.0"</span><span class="p">,</span> <span class="s2">"django"</span><span class="p">]</span>
<span class="n">packages</span><span class="o">=</span><span class="n">setuptools</span><span class="o">.</span><span class="n">find_packages</span><span class="p">(),</span>
<span class="n">classifiers</span><span class="o">=</span><span class="p">[</span>
<span class="s2">"Programming Language :: Python :: 3"</span><span class="p">,</span>
<span class="s2">"License :: OSI Approved :: MIT License"</span><span class="p">,</span>
<span class="s2">"Operating System :: OS Independent"</span><span class="p">,</span>
<span class="p">],</span>
<span class="n">python_requires</span><span class="o">=</span><span class="s1">'>=3.6'</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div>
<h2>Applications vs packages</h2>
<p>So we now know packages need only <code>install_requires</code> in <code>setup.py</code> in order to specify dependencies.
Application and standalone scripts need only <code>requirements.txt</code> for reproducing an environment.
Things get more complicated when your package is an application as well. <code>setuptools</code> can generate
a python script that can be placed in <code>$PATH</code>. It can call some entry point function in your
package. During the deployment we would like to have a reproducible environment and as I told
before <code>requirements.txt</code> is what we need to use to recreate such environments.</p>
<h3>pytest</h3>
<p>Let me consider the case that I started the post with. <code>pytest</code> is the best choice for developing
tests on any level: unit, functional and integration. The integration tests I worked on are really
huge. In order to manage the complexity we created some abstractions to interact with the web
application we want to test. These abstractions are organized as a package. In the tests we import
that package and in fact we test its code. Let me provide a simplified directory structure:</p>
<div class="highlight"><pre><span></span><code>mypkg/
__init__.py
app.py
view.py
tests/
test_app.py
test_view.py
setup.py
requirements.txt
</code></pre></div>
<p>Interesting thing here is that our tests is the application :) And for deploying our application
in CI we need to recreate the environment and that's why we have both <code>setup.py</code> and
<code>requirements.txt</code> in one repository. Of course nothing prevents you to divide the tests and the
package in different repositories but I think it would be inconvenient. </p>
<h2>References</h2>
<ul>
<li><a href="https://packaging.python.org/discussions/install-requires-vs-requirements/">https://packaging.python.org/discussions/install-requires-vs-requirements/</a></li>
<li><a href="https://caremad.io/2013/07/setup-vs-requirement/">https://caremad.io/2013/07/setup-vs-requirement/</a></li>
<li><a href="https://github.com/ManageIQ/integration_tests/">https://github.com/ManageIQ/integration_tests/</a></li>
</ul>
<p><a href="https://github.com/quarckster/blog.misharov.pro/discussions/7">Discuss on Github</a></p>OAuth in OpenShift2020-12-19T00:00:00+01:002020-12-19T00:00:00+01:00tag:blog.misharov.pro,2020-12-19:/2020-12-19/openshift-oauthApplications provisioned in OpenShift can use its OAuth authentication mechanism for granting
access. This is very useful feature because all applications running on OpenShift will have
consistent behavior. But there might be some caveats if you don't understand fully how OAuth works.<p>Applications provisioned in OpenShift can use its OAuth authentication mechanism for granting
access. This is very useful feature because all applications running on OpenShift will have
consistent behavior. But there might be some caveats if you don't understand fully how OAuth works.</p>
<h2>OAuth</h2>
<p>OAuth 2.0 is a quite complex authorization framework that allows a third-party application to obtain
limited access to an HTTP service. The very first step in authorization flow is a sending request
to authorization endpoint:</p>
<div class="highlight"><pre><span></span><code>GET {Authorization Endpoint}
?response_type=code // - Required
&client_id={Client ID} // - Required
&redirect_uri={Redirect URI} // - Conditionally required
&scope={Scopes} // - Optional
&state={Arbitrary String} // - Recommended
&code_challenge={Challenge} // - Optional
&code_challenge_method={Method} // - Optional
HTTP/1.1
HOST: {Authorization Server}
</code></pre></div>
<p>OpenShift allows you to configure a service account as an OAuth client. When using a service account
as an OAuth client:</p>
<ul>
<li><code>client_id</code> is <code>system:serviceaccount:<service_account_namespace>:<service_account_name></code>;</li>
<li><code>redirect_uri</code> must match an annotation on the service account.</li>
</ul>
<p>This is quite important information that you might need for your OpenShift administration routines.</p>
<h2>Again Jenkins</h2>
<p>OpenShift comes with a bunch of services that you can provision in one click. Jenkins is one of such
services. Moreover, it's well integrated and supports authentication using OpenShift OAuth. During
provisioning all required resources are created automatically including Deployment Config, Service
Account, Service, Route and others. After the provisioning your Jenkins instance will be
available on such URL <code>https://jenkins-your-namespace.apps.example.com</code>. But before you get to
Jenkins UI the very first request will be send to OpenShift authorization endpoint:</p>
<div class="highlight"><pre><span></span><code>https://oauth-openshift.apps.example.com/oauth/authorize
?client_id=system:serviceaccount:your-namespace:jenkins
&redirect_uri=https://jenkins-your-namespace.apps.example.com/securityRealm/finishLogin
&response_type=code
&scope=user:info user:check-access
&state=Y2MzNTQ4ZjYtMDY2ZC00
</code></pre></div>
<p>As you can see that URL exactly follows OAuth workflow. Let's imagine that we would like make our
Jenkins instance be available under a different hostname, e.g. <code>jenkins.example.org</code>. First of all
we need to create a new Route that has <code>jenkins.example.org</code> in the <code>host</code> field:</p>
<div class="highlight"><pre><span></span><code><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Route</span>
<span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">route.openshift.io/v1</span>
<span class="nt">metadata</span><span class="p">:</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">jenkins-example-org</span>
<span class="nt">spec</span><span class="p">:</span>
<span class="w"> </span><span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">jenkins.example.org</span>
<span class="w"> </span><span class="nt">to</span><span class="p">:</span>
<span class="w"> </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Service</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">jenkins</span>
<span class="w"> </span><span class="nt">weight</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">100</span>
<span class="w"> </span><span class="nt">port</span><span class="p">:</span>
<span class="w"> </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">web</span>
<span class="w"> </span><span class="nt">tls</span><span class="p">:</span>
<span class="w"> </span><span class="nt">termination</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">edge</span>
<span class="w"> </span><span class="nt">insecureEdgeTerminationPolicy</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Redirect</span>
<span class="w"> </span><span class="nt">wildcardPolicy</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">None</span>
</code></pre></div>
<p>Then you should add a CNAME record into <code>example.org</code> domain that points to Router Canonical
Hostname. If you try to open <code>https://jenkins.example.org</code> OpenShift OAuth proxy returns status code
400 with a message about invalid request. But pay attention which URL was requested. It's completely
the same URL with the same parameters including <code>redirect_uri</code>. So what should we do in order to fix
that? Jenkins template uses a Service Account as an OAuth client. The documentation says that the
Service Account should have an annotation with a key that starts with one of the following prefixes:</p>
<ul>
<li><code>serviceaccounts.openshift.io/oauth-redirecturi</code>.</li>
<li><code>serviceaccounts.openshift.io/oauth-redirectreference</code>.</li>
</ul>
<p>Let's check what we have in the Service Account for our Jenkins instance:</p>
<div class="highlight"><pre><span></span><code><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">ServiceAccount</span>
<span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">v1</span>
<span class="nt">metadata</span><span class="p">:</span>
<span class="w"> </span><span class="nt">annotations</span><span class="p">:</span>
<span class="w"> </span><span class="nt">serviceaccounts.openshift.io/oauth-redirectreference.jenkins</span><span class="p">:</span><span class="w"> </span><span class="p p-Indicator">>-</span>
<span class="w"> </span><span class="no">{"kind":"OAuthRedirectReference","apiVersion":"v1","reference":{"kind":"Route","name":"jenkins"}}</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">jenkins</span>
<span class="nn">...</span>
</code></pre></div>
<p>Thus we just need to replace the reference name of the Route. Resulting annotation should look like:
<code>{"kind":"OAuthRedirectReference","apiVersion":"v1","reference":{"kind":"Route","name":"jenkins-example-org"}}</code>.
After that authentication will work on the new URL.</p>
<h2>References</h2>
<ul>
<li><a href="https://tools.ietf.org/html/rfc6749">https://tools.ietf.org/html/rfc6749</a></li>
<li><a href="https://docs.openshift.com/container-platform/4.6/authentication/using-service-accounts-as-oauth-client.html">https://docs.openshift.com/container-platform/4.6/authentication/using-service-accounts-as-oauth-client.html</a></li>
</ul>
<p><a href="https://github.com/quarckster/blog.misharov.pro/discussions/5">Discuss on Github</a></p>Working with OpenAPI2020-12-12T00:00:00+01:002020-12-12T00:00:00+01:00tag:blog.misharov.pro,2020-12-12:/2020-12-12/apicurioLast two years I've been working on various parts of cloud.redhat.com. It's a big web application
with bunch of services and all of them provide REST API. In order to standardize documentation
OpenAPI was chosen as a specification for describing REST API.
It's a nice thing and I really see the value in it but the more your service is getting bigger the
more complicated become editing of the specification. I found a helpful tool that might interest
you.<p>Last two years I've been working on various parts of cloud.redhat.com. It's a big web application
with bunch of services and all of them provide REST API. In order to standardize documentation
<a href="https://swagger.io/specification/">OpenAPI</a> was chosen as a specification for describing REST API.
It's a nice thing and I really see the value in it but the more your service is getting bigger the
more complicated become editing of the specification. I found a helpful tool that might interest
you.</p>
<h2>Specification</h2>
<p>If you want describe your REST API via OpenAPI you just need create one file in json or yaml
formats. Usually it's <code>openapi.json</code>. Having the specification strictly formalized according to a
schema opens vast opportunities for code generation. Swagger provides a powerful utility for
generating clients on all popular programming languages. I heavily use it in my test automation.
Another great feature is a nice human readable documentation that is also automatically generated.
Basically it is a good practice to have <code>openapi.json</code> in the same repository where the source code
of your service located. During the deploying the specification should be deployed as well. It means
that we have to keep the specification up to date. And here we can have some difficulties. Until
your service is small it's not a big deal to manually edit the spec. Things are getting more
complicated when responses have deep nested objects.</p>
<h2>Cure</h2>
<p>Fortunately, there are many tools for editing <code>openapi.json</code>. I tried several of them and would like
to recommend one. It's called <a href="https://www.apicur.io/">Apicurio</a> and it's a nice web based open
source designer for your OpenAPI specification. You can upload an existing file or start from the
scratch. Apicurio provides ability of collaborative editing. Another cool feature is creating data
types from examples. Just paste a response from your application and Apicurio construct a data type.
Of course it doesn't work as expected in all cases but it's still very useful. The tool covers
almost all OpenAPI schema and even if you cannot do something via UI there is always a tab with raw
json. For example there is no way to specify a hashmap so I had to manually write that part.
Nevertheless Apicurio made my life a bit easier and I hope it will do yours as well.</p>
<p><img class="image-center" alt="diagram 1" src="https://blog.misharov.pro/assets/img/2020-12-12-apicurio.png"/></p>
<h2>References</h2>
<ul>
<li><a href="https://swagger.io/docs/specification/about/">OpenAPI</a></li>
<li><a href="https://github.com/swagger-api/swagger-codegen">Swagger codegen</a></li>
<li><a href="https://studio.apicur.io/">Apicurio studio</a></li>
<li><a href="https://openapi.tools">OpenApi tools</a></li>
</ul>
<p><a href="https://github.com/quarckster/blog.misharov.pro/discussions/3">Discuss on Github</a></p>How I became Red Hat Certified Architect2020-11-22T00:00:00+01:002020-11-22T00:00:00+01:00tag:blog.misharov.pro,2020-11-22:/2020-11-22/rhcaWhen I started to work at Red Hat I familiarized with Red Hat Certification program. You can take
an exam related to some Red Hat product and if you pass you will get a certificate. Red Hat exams
differ form other certification programs in the industry. You get a system, e.g. RHEL, and you need
to configure and fix it according to the tasks in the exam. Besides it has some milestones that
give you motivation to take more exams.<p>When I started to work at Red Hat I familiarized with Red Hat Certification program. You can take
an exam related to some Red Hat product and if you pass you will get a certificate. Red Hat exams
differ form other certification programs in the industry. You get a system, e.g. RHEL, and you need
to configure and fix it according to the tasks in the exam. Besides it has some milestones that
give you motivation to take more exams.</p>
<h2>My path and motivation</h2>
<p>I have to mention that redhatters can take the exams without any charge. It's not mandatory and
obtaining certificates doesn't give any bonuses but it's a good way to test your skills and feed
your ego :). In the first year at Red Hat I saw a colleague in a polo shirt with some lettering. It
was "Red Hat Certified Architect" with certification id number. I wanted to get such polo too but
for that I was need to become a Red Hat Certified Architect :). In order to do that you have to pass
Red Hat Certified Engineer exam and five other certificate of expertise exams. It took four and a
half years for me to meet the conditions and get that coveted polo. Here are the exams I passed:</p>
<ul>
<li>Red Hat Certified System Administrator</li>
<li>Red Hat Certified Engineer</li>
<li>Red Hat Certified Specialist in Hybrid Cloud Management</li>
<li>Red Hat Certified Specialist in Ansible Automation</li>
<li>Red Hat Certified Specialist in Linux Diagnostics and Troubleshooting</li>
<li>Red Hat Certified Specialist in OpenShift Application Development</li>
<li>Red Hat Certified Specialist in OpenShift Administration</li>
</ul>
<p>There are several ways how you can prepare for an exam and take it. I think I tried all of them.
I was in instructor-led classes, usually the class takes four days and on fifth day you take the
exam. An exam can be also taken on a kiosk machine placed at Red Hat offices or a partner’s site.
These exams are monitored remotely by a proctor. Recently due to covid pandemic it's also possible
to take an exam right at your home. And that's very cool. I passed last two OpenShift exams at home
because all kiosks were closed.</p>
<p>One thing you should know about certificates that they have expiration date. It means you have to
take new exams from time to time if you want keep your qualification. And it makes sense. The
industry does not stand still. New technologies appear, others are deprecated and you need to learn
new stuff all the time. Only one thing I miss in Red Hat certification program. It's pity RHCA is
the highest possible qualification. Such achievements are great motivation for people like me.</p>
<p><img class="image-center" alt="diagram 1" src="https://blog.misharov.pro/assets/img/2020-11-22-rhca.png"/></p>
<h2>References</h2>
<ul>
<li><a href="https://www.redhat.com/en/services/certifications">Red Hat Certification program</a></li>
<li><a href="https://www.redhat.com/en/services/certification/rhca">Red Hat Certified Architect page</a></li>
<li><a href="https://rhtapps.redhat.com/verify/?certId=160-194-544">My Red Hat certification profile</a></li>
</ul>
<p><a href="https://github.com/quarckster/blog.misharov.pro/discussions/1">Discuss on Github</a></p>Migrating Jenkins jobs to new instance2020-11-04T00:00:00+01:002020-11-04T00:00:00+01:00tag:blog.misharov.pro,2020-11-04:/2020-11-04/migrating-jenkinsYou might know Why I don't like Jenkins but I
still have to use it in my job. Recently I was need to migrate Jenkins jobs to new instance and it
was easier than I expected.<p>You might know <a href="https://blog.misharov.pro/2020-06-02/why-i-dont-like-jenkins">Why I don't like Jenkins</a> but I
still have to use it in my job. Recently I was need to migrate Jenkins jobs to new instance and it
was easier than I expected.</p>
<h2>Prerequisites</h2>
<p>I had two Jenkins instances on different OpenShift clusters. Both instances have mounted persistent
volumes where the content of <code>/var/lib/jenkins</code> directories is stored. I got a task to migrate all
jobs, configs and secrets to the new instance. I've never done that before so I went the dumbest
way I could figure out.</p>
<h2>Jobs migration</h2>
<p>Jenkins jobs are stored in <code>/var/lib/jenkins/job</code> so I decided just to copy it to some intermediate
storage. We use regular OpenShift Jenkins deployment so <code>rsync</code> was installed into the image. In
order to copy files between an OpenShift pod and the local machine <code>oc rsync</code> command should be
used. So here is my step by step guide:</p>
<ol>
<li>Use a machine with enough free space and fast connection.</li>
<li>Login to first OpenShift cluster:</li>
</ol>
<p><code>sh
oc login --token=some_token --server=ocp_api_url</code></p>
<ol>
<li>
<p>Switch the namespace and copy <code>/var/lib/jenkins/jobs</code>:</p>
<p><code>sh
oc project your_jenkins_project
oc rsync jenkins-pod-name:/var/lib/jenkins/jobs/ /some/local/directory</code></p>
</li>
<li>
<p>Login to another OpenShift cluster.</p>
</li>
<li>
<p>Switch namespace and copy <code>/some/local/directory</code> to the jenkins pod:</p>
<p><code>sh
oc project your_jenkins_project
oc rsync /some/local/directory jenkins-pod-name:/var/lib/jenkins/jobs/</code></p>
</li>
<li>
<p>Restart Jenkins on the second cluster.</p>
</li>
</ol>
<p>An that's it. You even don't need to stop first Jenkins instance. Actually if it was need to do it
would make the process more complicated. You can access data stored in PVCs only via pods that mount
them somewhere in the filesystem.</p>
<h2>Secrets migration</h2>
<p>All of our jobs use Jenkins secrets engine. Without secrets from the first instance jobs wouldn't
work. Fortunately, secrets migration was just about copying files. I found a helpful guide on
<a href="https://itsecureadmin.com">itsecureadmin.com</a>:</p>
<ol>
<li>
<p>Remove the <code>identity.key.enc</code> file on second instance:</p>
<p><code>sh
rm /var/lib/jenkins/identity.key.enc</code></p>
</li>
<li>
<p>Using <code>oc rsync</code> replace <code>secret*</code> <code>credentials.xml</code> files from first Jenkins instance to second
one.</p>
</li>
<li>Restart Jenkins on the second cluster.</li>
<li>...</li>
<li>PROFIT!</li>
</ol>
<h2>References</h2>
<ul>
<li><a href="https://itsecureadmin.com/2018/03/jenkins-migrating-credentials/">https://itsecureadmin.com/2018/03/jenkins-migrating-credentials/</a></li>
</ul>Triggering Kubernetes resources in OpenShift2020-10-09T00:00:00+02:002020-10-09T00:00:00+02:00tag:blog.misharov.pro,2020-10-09:/2020-10-09/ocp-trigger-annotationWhen I was starting to familiarize with OpenShift 3 I loved its builtin continous delivery
mechanism. Using webhooks and triggers you can easily deploy your code from Github, GitLab or
Bitbucket. In OpenShift 4 more Kubernetes resources have been added and they don't have triggers
support. Is there a way to enable it?<p>When I was starting to familiarize with OpenShift 3 I loved its builtin continous delivery
mechanism. Using webhooks and triggers you can easily deploy your code from Github, GitLab or
Bitbucket. In OpenShift 4 more Kubernetes resources have been added and they don't have triggers
support. Is there a way to enable it?</p>
<h2>Triggers</h2>
<p>Let me shortly explain what are triggers. Generally speaking it's just a section in yaml
that describes events inside the cluster that should drive the creation of new deployment processes.</p>
<p>Some examples:</p>
<div class="highlight"><pre><span></span><code><span class="nt">triggers</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="s">"ConfigChange"</span>
</code></pre></div>
<p>This trigger starts a new roll-out when we change DeploymentConfig spec.</p>
<div class="highlight"><pre><span></span><code><span class="nt">triggers</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="s">"ImageChange"</span>
<span class="w"> </span><span class="nt">imageChangeParams</span><span class="p">:</span>
<span class="w"> </span><span class="nt">automatic</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">true</span>
<span class="w"> </span><span class="nt">from</span><span class="p">:</span>
<span class="w"> </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="s">"ImageStreamTag"</span>
<span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s">"some_image:latest"</span>
<span class="w"> </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="s">"myproject"</span>
</code></pre></div>
<p>This trigger is more interesting. It starts a new roll-out when an image is updated in a certain
image stream.</p>
<p>The issue here is <code>triggers</code> only defined in <code>DeploymentConfig</code> which is OpenShift specific. If you
want to use them for Kubernetes resources such as <code>Deployment</code> you have to configured it
differenetely.</p>
<h2>Annotations</h2>
<p>OpenShift developers propose to add image change triggers into annotations. There is an especial
annotation <code>image.openshift.io/triggers</code>:</p>
<div class="highlight"><pre><span></span><code><span class="nt">image.openshift.io/triggers</span><span class="p">:</span><span class="w"> </span><span class="s">'[{"from":{"kind":"ImageStreamTag","some_image":"latest"},"fieldPath":"spec.template.spec.containers[?(@.name==\"myapp\")].image"}]'</span>
</code></pre></div>
<p>Fortunately, you don't have to remember this structure just <code>oc</code> command:</p>
<div class="highlight"><pre><span></span><code>$
oc<span class="w"> </span><span class="nb">set</span><span class="w"> </span>triggers<span class="w"> </span>deployment/myapp<span class="w"> </span>--from-image<span class="w"> </span>some_image:latest<span class="w"> </span>-c<span class="w"> </span>myapp
</code></pre></div>
<p>This command adds required annotation into your <code>Deployment</code>.</p>
<h2>References</h2>
<ul>
<li><a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/">https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/</a></li>
<li><a href="https://developers.redhat.com/blog/2019/09/20/using-red-hat-openshift-image-streams-with-kubernetes-deployments/">https://developers.redhat.com/blog/2019/09/20/using-red-hat-openshift-image-streams-with-kubernetes-deployments/</a></li>
<li><a href="https://docs.openshift.com/container-platform/3.11/dev_guide/deployments/basic_deployment_operations.html#triggers">https://docs.openshift.com/container-platform/3.11/dev_guide/deployments/basic_deployment_operations.html#triggers</a></li>
</ul>Open UI Automation2020-09-26T00:00:00+02:002020-09-26T00:00:00+02:00tag:blog.misharov.pro,2020-09-26:/2020-09-26/ouiaI guess all of you who have worked on UI testing know the "locator" problem. In order to find an
element in the DOM you need to provide a locator and usually it's an XPath string. Sometimes that
xpath can be quite nontrivial and in one day developers change the code and your locator cannot
find the required element. In order to avoid such situations as much as possible my colleagues
Peter Savage, Ronny Pfannschmidt and Karel Hala have developed a specification for frontend
developers Open Web UI Design Specification for Enabling Automation (OUIA). If your application
complies with OUIA it will have predictable locators and the behavior. This significantly simplifies
writing automated tests.<p>I guess all of you who have worked on UI testing know the "locator" problem. In order to find an
element in the DOM you need to provide a locator and usually it's an XPath string. Sometimes that
xpath can be quite nontrivial and in one day developers change the code and your locator cannot
find the required element. In order to avoid such situations as much as possible my colleagues
Peter Savage, Ronny Pfannschmidt and Karel Hala have developed a specification for frontend
developers Open Web UI Design Specification for Enabling Automation (OUIA). If your application
complies with OUIA it will have predictable locators and the behavior. This significantly simplifies
writing automated tests.</p>
<h2>The specification</h2>
<p>You can familiarize the specification here <a href="https://ouia.readthedocs.io/">https://ouia.readthedocs.io/</a>. Below I will describe a
part that relates to components and component frameworks.</p>
<h2>OUIA components</h2>
<p>The smallest building block of a front-end application is a component. It can be a button, dropdown
or even just a text block. If we want to interact with it we should find the root of the element in
the DOM. As I wrote above an xpath string is the most versatile way to locate it. Consider the
following example:</p>
<div class="highlight"><pre><span></span><code>.//div[@class="pf-c-button"]
</code></pre></div>
<h3>data-component-id</h3>
<p>This xpath finds all elements with <code>div</code> tag that has "pf-c-button" in <code>class</code> attribute. If you
need a specific element you can specify an index:</p>
<div class="highlight"><pre><span></span><code>.//div[@class="pf-c-buton"][1]
</code></pre></div>
<p>Such queries have several problems. First, there are might be situations when elements on the page
are shuffled on every reload and in the next test session your automation will fail. Another issue
with indexed xpath queries that they don't tell us anything about the underneath object's id. The
most common example would be a table. Usually tables represent data from databases. In your
automation test suite you often manipulate objects that abstract your testing application objects.
Thus we as testers would like to see some unique id in the root HTML tag. We could use <code>id</code>
attribute but in some cases it can be reserved for other purposes. It's better to use our own
attrbiute which we can control. <code>data-ouia-component-id</code> is such attribute. If it's one of many
buttons we could assign a variant of the button:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">button</span> <span class="na">data-ouia-component-id</span><span class="o">=</span><span class="s">"Submit"</span><span class="p">></</span><span class="nt">button</span><span class="p">></span>
</code></pre></div>
<p>If it's a row in a table that represents data from some database we might want to have the id of the
row matches to the id from the database, e.g. uuid:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">tr</span> <span class="na">data-ouia-component-id</span><span class="o">=</span><span class="s">"28b9fe89-da52-4dcf-aafe-9f675d708d09"</span><span class="p">></span>
<span class="p"><</span><span class="nt">td</span><span class="p">></span>...<span class="p"></</span><span class="nt">td</span><span class="p">></span>
<span class="p"></</span><span class="nt">tr</span><span class="p">></span>
</code></pre></div>
<h3>data-component-type</h3>
<p>Such components as dropdowns, navigations, paginators and others have complex behavior and their
root elements in the most cases don't tell us what exact component we work with. Of course we can
assume the component type from class attribute or other indicators but it always requires adding
some logic in a test suite. OUIA introduces <code>data-component-type</code> attribute. Let's consider
Patternfly's <a href="http://patternfly-react.surge.sh/components/dropdown#basic">Dropdown</a> component:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"pf-c-dropdown pf-m-expanded"</span> <span class="na">data-ouia-component-type</span><span class="o">=</span><span class="s">"PF4/Dropdown"</span>
<span class="na">data-ouia-component-id</span><span class="o">=</span><span class="s">"some_id"</span><span class="p">><</span><span class="nt">button</span> <span class="na">data-ouia-component-type</span><span class="o">=</span><span class="s">"PF4/DropdownToggle"</span>
<span class="na">data-ouia-component-id</span><span class="o">=</span><span class="s">"some_id"</span> <span class="na">class</span><span class="o">=</span><span class="s">"pf-c-dropdown__toggle"</span> <span class="na">type</span><span class="o">=</span><span class="s">"button"</span>
<span class="na">aria-expanded</span><span class="o">=</span><span class="s">"true"</span> <span class="na">aria-haspopup</span><span class="o">=</span><span class="s">"true"</span><span class="p">><</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">"pf-c-dropdown__toggle-text"</span><span class="p">></span>Dropdown<span class="p"></</span><span class="nt">span</span><span class="p">></</span><span class="nt">button</span><span class="p">></span>
<span class="p"><</span><span class="nt">ul</span> <span class="na">aria-labelledby</span><span class="o">=</span><span class="s">"toggle-id"</span> <span class="na">class</span><span class="o">=</span><span class="s">"pf-c-dropdown__menu"</span> <span class="na">role</span><span class="o">=</span><span class="s">"menu"</span><span class="p">></span>
<span class="p"><</span><span class="nt">li</span> <span class="na">role</span><span class="o">=</span><span class="s">"menuitem"</span><span class="p">><</span><span class="nt">a</span> <span class="na">tabindex</span><span class="o">=</span><span class="s">"-1"</span> <span class="na">data-ouia-component-type</span><span class="o">=</span><span class="s">"PF4/DropdownItem"</span> <span class="na">data-ouia-safe</span><span class="o">=</span><span class="s">"true"</span>
<span class="na">data-ouia-component-id</span><span class="o">=</span><span class="s">"some_id"</span> <span class="na">aria-disabled</span><span class="o">=</span><span class="s">"false"</span>
<span class="na">class</span><span class="o">=</span><span class="s">"pf-c-dropdown__menu-item"</span><span class="p">></span>Link<span class="p"></</span><span class="nt">a</span><span class="p">></</span><span class="nt">li</span><span class="p">></span>
<span class="p"></</span><span class="nt">ul</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</code></pre></div>
<p>As you can see it has <code>div</code> as a root element and in order to recongize that it's a Dropdown we
could get value of class attribute which is <code>pf-c-dropdown pf-m-expanded</code> and extract the name of
the component from it.
<code>data-ouia-component-type</code> explicitely tells us that we deal with Dropdown. <code>PF4</code> part specifies
a namespace or name of component library. In case of Patternfly it's <code>PF4</code>. It might be possible
that a page can have the same components from different libraries therefore it would be good to
separate them.</p>
<h3>data-ouia-safe</h3>
<p>In <a href="https://blog.misharov.pro/2020-06-18/selenium-please-wait">Selenium please wait!</a> I described a problem
with animated elements. We should wait when the animation is finished and only after that we can
continue to interact with the element. <code>data-ouia-safe</code> must be <code>true</code> only when a component doesn't
play any animation. It seems it's the most tricky part because it requires some additional
javascript code that will set the value for that attribute.</p>
<h3>OUIA xpath</h3>
<p>Combining all these pieces together we can have a very minimal versatile xpath for a given
component with some id. Here is it:</p>
<div class="highlight"><pre><span></span><code>.//*[@data-ouia-component-type="Some Component" and @data-ouia-component-id="some id"]
</code></pre></div>
<p>We can use this xpath in a test suites and frameworks. Moreover OUIA opens vast possibilities for
code generation. We can create a sort of DOM scanner that will generate ready-to-use abstractions
for testing frameworks.</p>
<h2>OUIA in the wild</h2>
<p>The first component library that started add OUIA attributes is <strong>Patternfly</strong>. We closely work
together with Patternlfy developers to provide better experience with OUIA to library consumers and
testers. Many thanks to <a href="https://github.com/redallen">@zallen</a> and
<a href="https://github.com/jschuler">@jschuller</a>. Besides there is ongoing work to adding OUIA
compatibility layer in various test suites and frameworks such as <strong>widgetastic.core</strong> and
<strong>widgetastic.patternfly4</strong>. I hope the specification will go beyond of Red Hat's projects and other
people will see the benefits of using OUIA.</p>
<h2>References</h2>
<ul>
<li><a href="https://ouia.readthedocs.io/">https://ouia.readthedocs.io/</a></li>
<li><a href="https://github.com/patternfly/patternfly-react/">https://github.com/patternfly/patternfly-react/</a></li>
<li><a href="https://github.com/RedHatQE/widgetastic.core/pull/177">https://github.com/RedHatQE/widgetastic.core/pull/177</a></li>
</ul>Making a Kodi add-on repository2020-08-09T00:00:00+02:002020-08-09T00:00:00+02:00tag:blog.misharov.pro,2020-08-09:/2020-08-09/kodi-add-on-repositoryIn this post I'll explain how to create your own Kodi add-on repository, deploy it on Netlify and
automate that workflow.<p>In this post I'll explain how to create your own Kodi add-on repository, deploy it on Netlify and
automate that workflow.</p>
<h2>Kodi</h2>
<p>Kodi is a popular open source multimedia center. It has a huge add-ons ecosystem which can extend
functionality significantly. Add-ons are distributed as regular zip archives and you can install it
via Kodi UI just specifying a path in your filesystem. That approach has one disadvantage. If you
want to update add-on version you have to do it manually. I mean you should follow updates of the
add-on, download and install it every time.</p>
<p>In order to get rid off all of these manual actions we can create an add-ons repository and enable
it in Kodi.</p>
<h2>Repository add-on structure</h2>
<p>According to <a href="https://kodi.wiki/view/Add-on_repositories">Kodi wiki</a> an add-on repo should have some
add-ons, a master xml file and a checksum of that file. Directory structure should look like this:</p>
<div class="highlight"><pre><span></span><code>repo_dir
├── addons.xml
├── addons.xml.md5
└── addon.id
└──addon.id-x.y.z.zip
...
└── addon2.id
└── addon2.id-x.y.z.zip
</code></pre></div>
<p>You can also put a repository add-on for distribution. This allows you to share your repository with
others.</p>
<h2>Hosting</h2>
<p>Add-on repository should be hosted on some web server. Static content hosting services such as
Github Pages or Netlify perfectly fit for this task. I prefer Netlify because it gives more control
for the deployment and its starter tariff plan is more than enough for our purposes. By the way
this blog is also hosted on Netlify.</p>
<p>Netlify provides a nice CLI tool for the deployment. First you should install <code>netlify-cli</code>. It's
a javascript application so we use <code>npm</code> here:</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>npm<span class="w"> </span>install<span class="w"> </span>netlify-cli
$
</code></pre></div>
<p>Then you can deploy a local folder to your site in the following way:</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>node_modules/netlify-cli/bin/run<span class="w"> </span>deploy<span class="w"> </span>--dir<span class="o">=</span>your_directory<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>--prod<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>--auth<span class="o">=</span><span class="s2">"</span><span class="nv">$NETLIFY_AUTH_TOKEN</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>--site<span class="o">=</span><span class="s2">"</span><span class="nv">$NETLIFY_SITE_ID</span><span class="s2">"</span>
$
</code></pre></div>
<p><code>$NETLIFY_AUTH_TOKEN</code> and <code>$NETLIFY_SITE_ID</code> you will find in Netlify settings. After that your
static files will be accessible via Internet. That's exactly what we need for making Kodi add-on
repository.</p>
<h2>Live example</h2>
<p>I created a Kodi add-on for one streaming service and in one day I decided to create an add-on
repository. I already had configured Travis CI and build script. I was need to extend it a bit.
A release pipeline is very simple:</p>
<div class="highlight"><pre><span></span><code>git push origin some_tag -> lint -> test -> deploy
</code></pre></div>
<p>In <code>deploy</code> stage an add-on archive is created and attached to Github Release as an asset. I
split it on <code>deploy_github</code> and <code>deploy_netlify</code>. In <code>deploy_netlify</code> the build script generates a
repo directory, put required files there and uploads everything to Netlify using <code>netlify-cli</code>.
Resulting pipeline:</p>
<div class="highlight"><pre><span></span><code>git push origin some_tag -> lint -> test -> deploy_github -> deploy_netlify
</code></pre></div>
<p>And the build script <code>make.sh</code>:</p>
<div class="highlight"><pre><span></span><code><span class="ch">#!/bin/bash</span>
<span class="c1"># I omitted the body of functions to show the idea.</span>
<span class="c1"># Full source can be found in the references below.</span>
<span class="k">function</span><span class="w"> </span>check_version<span class="o">()</span><span class="w"> </span><span class="o">{</span>
<span class="w"> </span>...
<span class="o">}</span>
<span class="k">function</span><span class="w"> </span>build_video_addon<span class="o">()</span><span class="w"> </span><span class="o">{</span>
<span class="w"> </span>check_version<span class="w"> </span><span class="nv">$1</span>
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"Creating video.kino.pub add-on archive"</span>
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"======================================"</span>
<span class="w"> </span>...
<span class="o">}</span>
<span class="k">function</span><span class="w"> </span>build_repo_addon<span class="o">()</span><span class="w"> </span><span class="o">{</span>
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"Creating repo.kino.pub add-on archive"</span>
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"====================================="</span>
<span class="w"> </span>...
<span class="o">}</span>
<span class="k">function</span><span class="w"> </span>create_repo<span class="o">()</span><span class="w"> </span><span class="o">{</span>
<span class="w"> </span>build_video_addon<span class="w"> </span><span class="nv">$1</span>
<span class="w"> </span>build_repo_addon
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"Creating repository add-on directory structure"</span>
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"=============================================="</span>
<span class="w"> </span>...
<span class="o">}</span>
<span class="k">function</span><span class="w"> </span>deploy<span class="o">()</span><span class="w"> </span><span class="o">{</span>
<span class="w"> </span>create_repo<span class="w"> </span><span class="nv">$1</span>
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"Deploying files to Netlify"</span>
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"=========================="</span>
<span class="w"> </span>node_modules/netlify-cli/bin/run<span class="w"> </span>deploy<span class="w"> </span>--dir<span class="o">=</span>repo<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>--prod<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>--auth<span class="o">=</span><span class="s2">"</span><span class="nv">$NETLIFY_AUTH_TOKEN</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>--site<span class="o">=</span><span class="s2">"</span><span class="nv">$NETLIFY_SITE_ID</span><span class="s2">"</span>
<span class="o">}</span>
<span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
</code></pre></div>
<p>In <code>deploy_github</code> CI runs <code>bash make.sh build_video_addon</code> and uploads the archive to Github. Then
in <code>deploy_netlify</code> <code>bash make.sh deploy</code> is called.</p>
<p>As you can see creating a Kodi add-on repository is a quite trivial task and updating it can be
easily automated.</p>
<h2>References</h2>
<ul>
<li><a href="https://kodi.tv">https://kodi.tv</a></li>
<li><a href="https://kodi.wiki/view/Add-on_development">https://kodi.wiki/view/Add-on_development</a></li>
<li><a href="https://kodi.wiki/view/Add-on_repositories">https://kodi.wiki/view/Add-on_repositories</a></li>
<li><a href="https://www.netlify.com/">https://www.netlify.com/</a></li>
<li><a href="https://github.com/quarckster/kodi.kino.pub">https://github.com/quarckster/kodi.kino.pub</a></li>
</ul>Evolution of UI testing2020-07-24T00:00:00+02:002020-07-24T00:00:00+02:00tag:blog.misharov.pro,2020-07-24:/2020-07-24/ui-testing-evolutionWeb UI testing is a tricky thing in many aspects. They requires many dependencies, they are slow and
they are often flaky. In this post I would like to share my web UI testing experience on an example
of one python library.<p>Web UI testing is a tricky thing in many aspects. They requires many dependencies, they are slow and
they are often flaky. In this post I would like to share my web UI testing experience on an example
of one python library.</p>
<h2>widgetastic.patternfly4</h2>
<p>I maintain a library that Red Hat uses for web UI testing. It's called <code>widgetastic.patternfly4</code> and
you can find it on Github. It abstracts some components from Patternfly design system.</p>
<h2>CI</h2>
<p>Every good library should have tests otherwise we cannot even know if something work there.
Therefore PR testing is a "must have" nowadays. There are many cloud base CI systems and I chose
Travis CI. I've already had some experience with it and that was a main reason of my choice.
All of these CI services give you a virtual machine where you can run your code. In the most of
cases preinstalled software is enough. The real fun begins when you need to install and setup
something that is missing.</p>
<p>This is a case of <code>widgetastic.patternfly4</code>. It has <code>selenium</code> in the dependencies. It means that
before a test session starts we need to install a browser (Chrome or Firefox) and an appropriate
webdriver (geckodriver or chromedriver). The very straightforward solution would be installing all
that stuff during the PR testing. My Travis CI config looked something like this at that time:</p>
<div class="highlight"><pre><span></span><code><span class="c1"># Travis CI provides an easy way to install a browser</span>
<span class="nt">addons</span><span class="p">:</span>
<span class="w"> </span><span class="nt">firefox</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">latest</span>
<span class="nt">language</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">python</span>
<span class="nn">...</span>
<span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">test</span>
<span class="nt">env</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">BROWSER=firefox</span>
<span class="nt">before_install</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">pip install -U setuptools pip</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">pip install flake8 pytest pytest-cov coveralls</span>
<span class="w"> </span><span class="c1"># I found an utility that downloads a right version of the webdriver</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">pip install webdriverdownloader</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">webdriverdownloader firefox</span>
<span class="nt">install</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">python setup.py install</span>
<span class="nt">script</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">flake8 src testing --max-line-length=100</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">py.test -v --cov widgetastic_patternfly4 --cov-report term-missing</span>
<span class="nn">...</span>
</code></pre></div>
<h2>Containers</h2>
<p>The aforementioned approach didn't work well. First it takes time to install all dependencies.
Second <code>webdriverdownloader</code> was buggy and often tests couldn't even start. Another issue was in the
way how browsers run. Obviously CI machine doesn't need any GUI therefore we have to run both Chrome
and Firefox in headless mode. In this mode the browser renders the pages in the memory. It turned
out that test results may differ significantly.</p>
<p>The solution was on the surface. Containers was already the industry standard and they perfectly fit
to my problem. Now I'm thinking why I didn't come up to it from the beginning 😅. So instead of
installing dependencies during PR testing it's better to pack everything what we need in a one
container image. I created such image and pushed it to <code>quay.io</code> image registry. After that CI
config became into:</p>
<div class="highlight"><pre><span></span><code><span class="nt">language</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">python</span>
<span class="nn">...</span>
<span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">test</span>
<span class="c1"># here we start docker service</span>
<span class="nt">services</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">docker</span>
<span class="nt">env</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">BROWSER=firefox</span>
<span class="nt">before_install</span><span class="p">:</span>
<span class="w"> </span><span class="c1"># The container has Selenium standalone running. In the tests we can use Remote webdriver to</span>
<span class="w"> </span><span class="c1"># connect to it. Moreover we can even have X server inside and run browsers in regular way.</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">docker run -d -p 4444:4444 -v /dev/shm:/dev/shm quay.io/redhatqe/selenium-standalone</span>
<span class="nt">install</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">pip install -U setuptools pip wheel</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">pip install pytest pytest-cov codecov</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">python setup.py install</span>
<span class="nt">script</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">pytest -v --no-cov-on-fail --cov=widgetastic_patternfly4</span>
<span class="nn">...</span>
</code></pre></div>
<h2>Can I do better?</h2>
<p>I was satisfied for while and switched to other projects. In the meantime the library was growing.
More components were added and tests as well. A session of 116 tests took more than <strong>20</strong> minutes.
I wanted to do something about it. It needs to know that tests are run sequentially, one by one.
This is what I tried to experiment with. <code>pytest</code> has a very nice plugin called <code>pytest-xdist</code>. It
can distribute tests among some number of workers. Ok, let's try it:</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>pip<span class="w"> </span>install<span class="w"> </span>pytest-xdist
$<span class="w"> </span>pytest<span class="w"> </span>-v<span class="w"> </span>-n<span class="w"> </span><span class="m">8</span>
</code></pre></div>
<p>It just started to execute the tests in parallel! But in my setup all tests were executed against
one container. That caused undesirable side effects and may tests failed. So we need to give a
dedicated container to each worker. For that I added a fixture that spawns a container on some local
host address and then python selenium uses it to interact with a browser. Here is the fixture:</p>
<div class="highlight"><pre><span></span><code><span class="nd">@pytest</span><span class="o">.</span><span class="n">fixture</span><span class="p">(</span><span class="n">scope</span><span class="o">=</span><span class="s2">"session"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">selenium_host</span><span class="p">(</span><span class="n">worker_id</span><span class="p">):</span>
<span class="n">oktet</span> <span class="o">=</span> <span class="mi">1</span> <span class="k">if</span> <span class="n">worker_id</span> <span class="o">==</span> <span class="s2">"master"</span> <span class="k">else</span> <span class="nb">int</span><span class="p">(</span><span class="n">worker_id</span><span class="o">.</span><span class="n">lstrip</span><span class="p">(</span><span class="s2">"gw"</span><span class="p">))</span> <span class="o">+</span> <span class="mi">1</span>
<span class="n">host</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"127.0.0.</span><span class="si">{</span><span class="n">oktet</span><span class="si">}</span><span class="s2">"</span>
<span class="n">ps</span> <span class="o">=</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">(</span>
<span class="p">[</span>
<span class="s2">"sudo"</span><span class="p">,</span>
<span class="s2">"podman"</span><span class="p">,</span>
<span class="s2">"run"</span><span class="p">,</span>
<span class="s2">"--rm"</span><span class="p">,</span>
<span class="s2">"-d"</span><span class="p">,</span>
<span class="s2">"-p"</span><span class="p">,</span>
<span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">host</span><span class="si">}</span><span class="s2">:4444:4444"</span><span class="p">,</span>
<span class="s2">"--shm-size=2g"</span><span class="p">,</span>
<span class="s2">"quay.io/redhatqe/selenium-standalone"</span><span class="p">,</span>
<span class="p">],</span>
<span class="n">stdout</span><span class="o">=</span><span class="n">subprocess</span><span class="o">.</span><span class="n">PIPE</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">yield</span> <span class="n">host</span>
<span class="n">container_id</span> <span class="o">=</span> <span class="n">ps</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">([</span><span class="s2">"sudo"</span><span class="p">,</span> <span class="s2">"podman"</span><span class="p">,</span> <span class="s2">"kill"</span><span class="p">,</span> <span class="n">container_id</span><span class="p">])</span>
</code></pre></div>
<p>As you can see I just get a worker id and make a host address from it. I use <code>podman</code> to manage
containers. Unfortunately, version 2.0.2 has a nasty <a href="https://github.com/containers/podman/issues/7016">bug</a>
that breaks networking in rootless containers. So I had to run containers with <code>sudo</code> but I'll
remove it when the bug will be fixed. Local experiments went fine. The next task is to prepare the
CI system. As I told earlier I was using Travis CI and it has one drawback. It doesn't have <code>podman</code>
preinstalled. Strictly speaking nothing could prevent me to use <code>docker</code> but I prefer <code>podman</code> due
to its daemonless approach and rootless containers.</p>
<p>In the end I switched to Github Actions. Their instances have <code>podman</code> preinstalled and Actions have
a nice feature of shared steps. But in other aspects I wouldn't highlight that CI system among
others. Here is an excerpt of <code>workflow.yaml</code>:</p>
<div class="highlight"><pre><span></span><code><span class="nn">...</span>
<span class="nt">test</span><span class="p">:</span>
<span class="w"> </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">ubuntu-latest</span>
<span class="w"> </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">lint</span>
<span class="w"> </span><span class="nt">strategy</span><span class="p">:</span>
<span class="w"> </span><span class="nt">matrix</span><span class="p">:</span>
<span class="w"> </span><span class="nt">browser</span><span class="p">:</span><span class="w"> </span><span class="p p-Indicator">[</span><span class="s">"firefox"</span><span class="p p-Indicator">,</span><span class="w"> </span><span class="s">"chrome"</span><span class="p p-Indicator">]</span>
<span class="w"> </span><span class="nt">steps</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">actions/checkout@v2</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">actions/setup-python@v2</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Install dependencies</span>
<span class="w"> </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p p-Indicator">|</span>
<span class="w"> </span><span class="no">pip install -U setuptools pip</span>
<span class="w"> </span><span class="no">pip install pytest pytest-cov codecov pytest-xdist</span>
<span class="w"> </span><span class="no">python setup.py install</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Test with pytest</span>
<span class="w"> </span><span class="nt">env</span><span class="p">:</span>
<span class="w"> </span><span class="nt">BROWSER</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">${{ matrix.browser }}</span>
<span class="w"> </span><span class="c1"># Containers are spawn from the tests now.</span>
<span class="w"> </span><span class="c1"># "-n 5" means that 5 workers are used with 5 dedicated containers.</span>
<span class="w"> </span><span class="c1"># "--dist=loadscope" means tests spread among workers by modules. Each worker executes tests</span>
<span class="w"> </span><span class="c1"># from the same module.</span>
<span class="w"> </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p p-Indicator">|</span>
<span class="w"> </span><span class="no">pytest -v -n 5 --dist=loadscope --no-cov-on-fail --cov=widgetastic_patternfly4 --cov-report=xml:/tmp/coverage.xml</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Publish coverage</span>
<span class="w"> </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">codecov/codecov-action@v1</span>
<span class="w"> </span><span class="nt">with</span><span class="p">:</span>
<span class="w"> </span><span class="nt">file</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">/tmp/coverage.xml</span>
<span class="nn">...</span>
</code></pre></div>
<p>So what about time execution? It's reduced to <strong>11</strong> minutes and it's two times faster! Not bad. Can
I do better?</p>
<h2>References</h2>
<ul>
<li><a href="https://github.com/RedHatQE/widgetastic.patternfly4">widgetastic.patternfly4</a></li>
<li><a href="https://github.com/pytest-dev/pytest-xdist">pytest-xdist</a></li>
<li><a href="https://www.patternfly.org/v4/documentation/react/overview/release-notes">Patternfly</a></li>
<li><a href="https://github.com/leonidessaguisagjr/webdriverdownloader">webdriverdownloader</a></li>
<li><a href="https://podman.io">podman</a></li>
<li><a href="https://github.com/RedHatQE/selenium-images">selenium-standalone</a></li>
</ul>Pipelines speedup2020-07-06T00:00:00+02:002020-07-06T00:00:00+02:00tag:blog.misharov.pro,2020-07-06:/2020-07-06/pipeline-speed-upOften when you want to run some tests you need to install required dependencies such as python
packages, ruby gems, npm packages and so on. Nowadays tests are executed automatically for pull
(merge) requests in various CI systems. Dependency pulling is repeated again and again for every
test run and it takes time. We cannot do much about it in cloud based CI systems such as Travis,
Circle CI or Github Actions but there is some space for improvement for self-hosted Gitlab CI,
Jenkins and others.<p>Often when you want to run some tests you need to install required dependencies such as python
packages, ruby gems, npm packages and so on. Nowadays tests are executed automatically for pull
(merge) requests in various CI systems. Dependency pulling is repeated again and again for every
test run and it takes time. We cannot do much about it in cloud based CI systems such as Travis,
Circle CI or Github Actions but there is some space for improvement for self-hosted Gitlab CI,
Jenkins and others.</p>
<h2>Cache</h2>
<p>The first idea that would come to your mind is to enable cache. It looks reasonable. Why do need to
redownload the same files again and again? Would it be better to store them in some near located
storage? The answer is yes but there are some nuances. First of all we should decide how our cache
mechanism will be implemented. I would highlight the following ways: caching proxy, cache in the
same filesystem and the combination of these two methods.</p>
<h3>Caching proxy</h3>
<p>Using proxy makes sense if your organization consumes a significant amount traffic from package
repositories. Having caching proxy reduces amount of incoming traffic and speeds up downloading.
Besides local caching proxy can play a very important role in storing proprietary packages. There
are several caching proxies and I worked with Sonatype Nexus and devpi.</p>
<p><img class="image-center" alt="diagram 1" src="https://blog.misharov.pro/assets/img/2020-07-10-pipeline-speed-up-1.png"/></p>
<p>The most of the time I work with Python therefore below I will provide examples for its package
infrastructure. As you know Python standard package manager is <code>pip</code> and in order to download
packages from a caching proxy you have to specify an index url by one of the following methods:</p>
<ul>
<li><code>pip.conf</code>:</li>
</ul>
<p><code>txt
[global]
index-url = https://example.com/pypi/packages</code></p>
<ul>
<li>environment variable: <code>PIP_INDEX_URL=https://example.com/pypi/packages</code></li>
<li>command line argument: <code>pip -i https://example.com/pypi/packages</code></li>
</ul>
<h3>Local cache</h3>
<p><code>pip</code>, <code>gem</code>, <code>yum</code> and others don't download packages from the index url immediately. Before that
they check if requested packages are stored locally in predefined directories of the file system.
Various package managers have different cache directories. <code>pip</code> in Linux stores downloads in
<code>$HOME/.cache/pip</code> by default. And again you can change that path via <code>pip.conf</code>, environment
variable <code>PIP_CACHE_DIR</code> or command line argument <code>--cache-dir</code>.</p>
<h2>Cache in pipelines</h2>
<p>That was a sort of preamble. Now we have a required knowledge in order to start a pipeline
optimization. Let's designate prerequisites:</p>
<ul>
<li>you use <a href="https://blog.misharov.pro/2020-05-08/gitlab-runner-in-openshift#gitlab-ci">Gitlab CI</a>;</li>
<li>Gitlab runner is configured to use kubernetes executor;</li>
<li>a pipeline has a stage with installing python packages:</li>
</ul>
<p><code>yaml
test:
image: python
stage: tests
before_script:
- pip install -U pip setuptools
- pip install -r requirements.txt
script:
- pytest</code></p>
<ul>
<li>there is a caching proxy that has the index url https://example.com/pypi/packages;</li>
<li>you have a PVC with shared access.</li>
</ul>
<p>First of all let's specify caching proxy index url in <code>.gitlab-ci.yml</code>:</p>
<div class="highlight"><pre><span></span><code><span class="nt">test</span><span class="p">:</span>
<span class="w"> </span><span class="nt">variables</span><span class="p">:</span>
<span class="w"> </span><span class="nt">PIP_INDEX_URL</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">https://example.com/pypi/packages</span>
</code></pre></div>
<p>Then we need to modify Gitlab runner config file in order to mount the PVC to builder pods:</p>
<div class="highlight"><pre><span></span><code><span class="k">[[runners]]</span>
<span class="w"> </span><span class="n">executor</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"kubernetes"</span>
<span class="w"> </span><span class="c1"># ...</span>
<span class="w"> </span><span class="n">cache_dir</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"/mnt/cache"</span>
<span class="w"> </span><span class="k">[runners.kubernetes]</span>
<span class="w"> </span><span class="c1"># ...</span>
<span class="w"> </span><span class="k">[runners.kubernetes.volumes]</span>
<span class="w"> </span><span class="c1"># ...</span>
<span class="w"> </span><span class="k">[[runners.kubernetes.volumes.pvc]]</span>
<span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"name_of_pvc"</span>
<span class="w"> </span><span class="n">mount_path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"/mnt/cache"</span>
</code></pre></div>
<p>It's a kind of an unobvious trick because according to the official <a href="https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section">documentation</a> <code>cache_dir</code> setting is only for Shell, Docker and SSH executors.
And you supposed to use an S3 backend for storing cache. I found that trick in
<a href="https://gitlab.com/gitlab-org/gitlab-runner/-/issues/1906#note_75349325">Kubernetes cache support</a>
issue.</p>
<p>And the final part is to modify the pipeline stage config:</p>
<div class="highlight"><pre><span></span><code><span class="nt">test</span><span class="p">:</span>
<span class="w"> </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">tests</span>
<span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">python</span>
<span class="w"> </span><span class="nt">cache</span><span class="p">:</span>
<span class="w"> </span><span class="c1"># that's the place where we tell Gitlab to treat that directory as a cache and</span>
<span class="w"> </span><span class="c1"># properly handle the content of the directory</span>
<span class="w"> </span><span class="nt">paths</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">.cache/pip</span>
<span class="w"> </span><span class="nt">variables</span><span class="p">:</span>
<span class="w"> </span><span class="c1"># Change pip's cache directory to be inside the project directory since we can</span>
<span class="w"> </span><span class="c1"># only cache local items.</span>
<span class="w"> </span><span class="nt">PIP_CACHE_DIR</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">$CI_PROJECT_DIR/.cache/pip</span>
<span class="w"> </span><span class="nt">before_script</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">pip install -U pip setuptools</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">pip install -r requirements.txt</span>
<span class="w"> </span><span class="nt">script</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">pytest</span>
</code></pre></div>
<p>Now in the pipeline log you should see something like that:</p>
<div class="highlight"><pre><span></span><code>Restoring cache
Checking cache for default...
No URL provided, cache will not be downloaded from shared cache server. Instead a
local version of cache will be extracted.
Successfully extracted cache
Downloading artifacts
Running before_script and script
$ pip install -U pip setuptools
Collecting pip
# we don't download the package but use already downloaded from prior run
Using cached https://example.com/pypi/packages/pip-20.1.1-py2.py3-none-any.whl
...
Saving cache
Creating cache default...
.cache/pip: found 1505 matching files
No URL provided, cache will be not uploaded to shared cache server. Cache will be
stored only locally.
Created cache
</code></pre></div>
<p>In the end we should get the following package flow:</p>
<p><img class="image-center" alt="diagram 2" src="https://blog.misharov.pro/assets/img/2020-07-10-pipeline-speed-up-2.png"/></p>
<p>Enabling cache in some cases can speed up to several times pipeline execution. Because not only
downloaded packages are cached but compiled as well.</p>Gitlab CI shared pipelines2020-07-05T00:00:00+02:002020-07-05T00:00:00+02:00tag:blog.misharov.pro,2020-07-05:/2020-07-05/gitliab-ci-pipeline-testingImagine you have a group of repositories in Gitlab and you would like to have the same pipelines
for whole group.<p>Imagine you have a group of repositories in Gitlab and you would like to have the same pipelines
for whole group.</p>
<h2>Includes</h2>
<p>The first thing that would come on your mind it's storing a pipeline configuration in a dedicated
git repository and then somehow consume it. Something similar has Jenkins for its scripted
pipelines. Gitlab developers thought in the same direction and introduced <code>include</code> keyword. It
supports several inclusion methods: local, file, remote and template.</p>
<h3>Local</h3>
<p>Let's say you have a repository with the following file structure:</p>
<div class="highlight"><pre><span></span><code>some_repository
├── .gitlab-ci.yml
├── lint.yml
├── tests.yml
└── deploy.yml
</code></pre></div>
<p>You can split your pipeline config on several files where an each file represents a stage. Using
<code>include:local</code> method it's possible to include content of the files in the same repository:</p>
<div class="highlight"><pre><span></span><code><span class="nt">include</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">local</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">/lint.yml</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">local</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">/tests.yml</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">local</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">/deploy.yml</span>
</code></pre></div>
<h3>File</h3>
<p><code>include:file</code> will be useful when you want to include a pipeline config from a different repository
under the same Gitlab instance.</p>
<div class="highlight"><pre><span></span><code><span class="nt">include</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">project</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">my-group/some_repository</span>
<span class="w"> </span><span class="nt">file</span><span class="p">:</span><span class="w"> </span><span class="s">'/.gitlab-ci.yml</span>
</code></pre></div>
<p>It's possible to specify ref which makes it very similar to <a href="https://www.jenkins.io/doc/book/pipeline/shared-libraries/#using-libraries">Jenkins' shared pipelines mechanism</a>:</p>
<div class="highlight"><pre><span></span><code><span class="nt">include</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">project</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">my-group/some_repository</span>
<span class="w"> </span><span class="nt">ref</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">stable</span>
<span class="w"> </span><span class="nt">file</span><span class="p">:</span><span class="w"> </span><span class="s">'/.gitlab-ci.yml</span>
</code></pre></div>
<h3>Remote</h3>
<p><code>include:remote</code> purpose is to include a pipeline config from an arbitrary git repository:</p>
<div class="highlight"><pre><span></span><code><span class="nt">include</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">remote</span><span class="p">:</span><span class="w"> </span><span class="s">'https://gitlab.com/awesome-project/raw/master/.gitlab-ci-template.yml'</span>
</code></pre></div>
<p>It's not very clear if you can specify a certain ref.</p>
<h3>Template</h3>
<p>Gitlab Ci comes with some <a href="https://gitlab.com/gitlab-org/gitlab/tree/master/lib/gitlab/ci/templates">predefined</a>
pipeline configs and you can include them using <code>include:template</code>:</p>
<div class="highlight"><pre><span></span><code><span class="nt">include</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">template</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Auto-DevOps.gitlab-ci.yml</span>
</code></pre></div>
<h2>Nested includes</h2>
<p>One of cool features of <code>include</code> is that you can nest this directive. According to documentation
100 nested includes is allowed. For example we have two repositories with the following file
structures:</p>
<div class="highlight"><pre><span></span><code>shared-pipeline
├── all.yml
├── lint.yml
├── tests.yml
└── deploy.yml
</code></pre></div>
<div class="highlight"><pre><span></span><code>my-python-project
├── src
├── tests
├── .gitlab-ci.yml
└── README.md
</code></pre></div>
<p>Then we can nest includes:</p>
<p><code>my-python-project/.gitlab-ci.yml</code>:</p>
<div class="highlight"><pre><span></span><code><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">project</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">my-group/shared-pipeline</span>
<span class="w"> </span><span class="nt">ref</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">master</span>
<span class="w"> </span><span class="nt">file</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">/all.yml</span>
</code></pre></div>
<p><code>shared-pipeline/all.yml</code>:</p>
<div class="highlight"><pre><span></span><code><span class="nt">include</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">local</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">/lint.yml</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">local</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">/tests.yml</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">local</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">/deploy.yml</span>
</code></pre></div>
<h2>With great power comes great responsibility</h2>
<p>Awesome, we reduced redundancies and kept our code "DRY". But now we have a new challenge. Imagine
we have dozens projects that use our shared pipeline config and if we break something in there we
break all pipelines in dependant repositories. Obviously we need to test our changes before we merge
them. It's kind of a tricky task because simple yaml lint test is not enough we need to run our
changes against a real repository.</p>
<h3>Testing repository</h3>
<p>I encountered with that problem and I decided to create a separate Gitlab repository in order to
test changes in shared pipeline library. So here are the prerequisites:</p>
<ul>
<li>a shared Gitlab pipeline config repo: <code>https://gitlab.com/acme/shared-pipeline-lib</code></li>
<li>a testing repository: <code>https:/gitlab.com/acme/pipeline-test</code></li>
<li>a user who wants to change <code>shared-pipeline-lib</code>: <code>some_user</code></li>
</ul>
<p>And here are the steps that you need to perform to test your MR:</p>
<ol>
<li>Create a merge request to <code>https://gitlab.com/acme/shared-pipeline-lib</code>.</li>
<li>
<p>Clone <code>pipeline-test</code> repo:</p>
<p><code>sh
git clone git@gitlab.cee.redhat.com:insights-qe/pipeline-test.git</code></p>
</li>
<li>
<p>Create a branch in <code>pipeline-test</code> repo with the following name:
<code>your_name/merge_request_branch_name</code>, e.g. <code>some_user/my_awesome_changes</code>:</p>
<p><code>sh
git checkout -b some_user/my_awesome_changes</code></p>
</li>
<li>
<p>Edit <code>.gitlab-ci.yml</code> in pipeline-test repo to point include to ref with your changes. e.g.:</p>
<p><code>yaml
include:
- project: some_user/shared-pipeline-lib
ref: my_awesome_changes
file: /all.yml</code></p>
</li>
<li>
<p>Commit the changes and push the branch directly to https://gitlab.cee.redhat.com/insights-qe/pipeline-test:</p>
<p><code>sh
git add .gitlab-ci.yml
git commit -m "Updated .gitlab-ci.yml"
git push origin HEAD</code></p>
</li>
<li>
<p>Now you can examine changes of your MR. Pipeline should automatically start and you should be
able to see it on <code>https:/gitlab.com/acme/pipeline-test</code></p>
</li>
</ol>
<p>I hope you got the idea. I guess there is a space for improvement and it's possible to automate
these steps.</p>
<h2>References</h2>
<ul>
<li><a href="https://docs.gitlab.com/ee/ci/yaml/README.html#include">https://docs.gitlab.com/ee/ci/yaml/README.html#include</a></li>
</ul>Selenium please wait!2020-06-18T00:00:00+02:002020-06-18T00:00:00+02:00tag:blog.misharov.pro,2020-06-18:/2020-06-18/selenium-please-waitWhen you test your web application with Selenium there is a one fundamental problem. Due to dynamic
nature of modern web pages you cannot interact with elements on the web page immediately after
loading. What can we do with that?<p>When you test your web application with Selenium there is a one fundamental problem. Due to dynamic
nature of modern web pages you cannot interact with elements on the web page immediately after
loading. What can we do with that?</p>
<h2>Details of the problem</h2>
<p>In old good times when javascript was used mostly for showing snowflakes Selenium was a perfect
solution for testing web UIs. Web pages were mostly static. When a web page was loaded by a browser
we could start interact with it via our automation suite. What do we have now? Modern frontends are
complex applications and the loaded web page doesn't mean that it's ready for automation. We should
wait until it be ready. But how long and what exactly should we wait for? I would highlight three
factors that can interfere in Selenium automation:</p>
<ol>
<li>Animation</li>
<li>Rendering</li>
<li>XHR requests</li>
</ol>
<h2>Animation</h2>
<p>As I wrote before we expect a static web page without any moving parts in order to successfully use
our Selenium based automation. Various animations might break it. For example a dropdown might
expand its content not immediately but after animation time. What we can do about it? Usually such
components designate their state via <code>class</code> html attribute. You can look how it's implemented in
<a href="https://getbootstrap.com/docs/4.5/components/collapse/#accordion-example">Twitter Bootstrap Accordion</a>.
Div tag of the collapsed group item has <code>collapse</code> in class attribute:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">"collapseOne"</span>
<span class="na">class</span><span class="o">=</span><span class="s">"collapse"</span>
<span class="na">aria-labelledby</span><span class="o">=</span><span class="s">"headingOne"</span>
<span class="na">data-parent</span><span class="o">=</span><span class="s">"#accordionExample"</span>
<span class="na">style</span><span class="o">=</span><span class="s">""</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</code></pre></div>
<p>During the animation it's changed to <code>collapsing</code>:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">"collapseOne"</span>
<span class="na">class</span><span class="o">=</span><span class="s">"collapsing"</span>
<span class="na">aria-labelledby</span><span class="o">=</span><span class="s">"headingOne"</span>
<span class="na">data-parent</span><span class="o">=</span><span class="s">"#accordionExample"</span>
<span class="na">style</span><span class="o">=</span><span class="s">""</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</code></pre></div>
<p>And expanded state has <code>collapse show</code> value.</p>
<p>This fact gives us ability to know exact time when we can work with the component. We could use one
libraries from <a href="https://blog.misharov.pro/2020-05-12/python-retry">Python retry!</a> in order to periodically
fetch value of <code>class</code> attribute. This method is not ideal. First of all, this method requires an
individual approach for every component that has the animation. Secondly, not all components change
their attributes during the animation.</p>
<h2>Rendering</h2>
<p>It's a quite new problem which I faced working with React based UI. A component is not shown
right after <a href="#xhr">XHR</a> because javascript code needs some time to render the HTML. There is no easy
solution except periodically polling the web page to define if the component is displayed or not:</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">selenium</span> <span class="kn">import</span> <span class="n">webdriver</span>
<span class="kn">from</span> <span class="nn">selenium.common.exceptions</span> <span class="kn">import</span> <span class="n">NoSuchElementException</span>
<span class="kn">from</span> <span class="nn">wait_for</span> <span class="kn">import</span> <span class="n">wait_for_decorator</span>
<span class="n">driver</span> <span class="o">=</span> <span class="n">webdriver</span><span class="o">.</span><span class="n">Firefox</span><span class="p">()</span>
<span class="n">driver</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"http://some_url"</span><span class="p">)</span>
<span class="nd">@wait_for_decorator</span><span class="p">(</span><span class="n">delay</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">60</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">find_element</span><span class="p">():</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">element</span> <span class="o">=</span> <span class="n">driver</span><span class="o">.</span><span class="n">find_element_by_xpath</span><span class="p">(</span><span class="s2">".//div"</span><span class="p">)</span>
<span class="k">except</span> <span class="n">NoSuchElementException</span><span class="p">:</span>
<span class="k">return</span> <span class="kc">False</span>
<span class="k">return</span> <span class="n">element</span><span class="o">.</span><span class="n">is_displayed</span><span class="p">()</span>
</code></pre></div>
<h2>XHR</h2>
<p>I don't want to dive into <a href="https://en.wikipedia.org/wiki/XMLHttpRequest">details</a> of this API the
only thing is important to us as testers that javascript uses it to modify a web page without
reloading. It causes serious problems because Selenium doesn't provide any method for your
automation that would allow you to know when XHR is started and finished. There are some indirect
ways to do that. You can look up an element on the web page until it will appear. It's a quite slow
method and it's not error prone. Some libraries provide an API for XHR. For instance when jQuery
executions are completed, you will have the initial <code>jQuery.active == 0</code>. But what if our UI doesn't
use jQuery but React or Angular?</p>
<p>We can fundamentally solve this problem by changing the frontend code. The idea is to intercept all
XHR requests and track their states in some variable which we can read in Selenium. We made this in
<a href="https://github.com/RedHatInsights/insights-chrome/blob/master/src/js/iqeEnablement.js">cloud.redhat.com</a>:</p>
<div class="highlight"><pre><span></span><code><span class="kd">let</span><span class="w"> </span><span class="nx">xhrResults</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[];</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">fetchResults</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{};</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">initted</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span><span class="p">;</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">wafkey</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">null</span><span class="p">;</span>
<span class="kd">function</span><span class="w"> </span><span class="nx">init</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'[iqe] initialized'</span><span class="p">);</span>
<span class="w"> </span><span class="c1">// Here we substitute original XHR methods and also "fetch"</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">open</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">window</span><span class="p">.</span><span class="nx">XMLHttpRequest</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">open</span><span class="p">;</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">send</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">window</span><span class="p">.</span><span class="nx">XMLHttpRequest</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">send</span><span class="p">;</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">oldFetch</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">window</span><span class="p">.</span><span class="nx">fetch</span><span class="p">;</span>
<span class="w"> </span><span class="c1">// must use function here because arrows dont "this" like functions</span>
<span class="w"> </span><span class="nb">window</span><span class="p">.</span><span class="nx">XMLHttpRequest</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">open</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">openReplacement</span><span class="p">(</span><span class="nx">_method</span><span class="p">,</span><span class="w"> </span><span class="nx">url</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">_url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">url</span><span class="p">;</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">req</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">open</span><span class="p">.</span><span class="nx">apply</span><span class="p">(</span><span class="k">this</span><span class="p">,</span><span class="w"> </span><span class="nx">arguments</span><span class="p">);</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">wafkey</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">setRequestHeader</span><span class="p">(</span><span class="nx">wafkey</span><span class="p">,</span><span class="w"> </span><span class="mf">1</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">req</span><span class="p">;</span>
<span class="w"> </span><span class="p">};</span>
<span class="w"> </span><span class="c1">// must use function here because arrows dont "this" like functions</span>
<span class="w"> </span><span class="nb">window</span><span class="p">.</span><span class="nx">XMLHttpRequest</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">send</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">sendReplacement</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// Here we put the request object into a xhrResults array where we can track the states of</span>
<span class="w"> </span><span class="c1">// all requests</span>
<span class="w"> </span><span class="nx">xhrResults</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">send</span><span class="p">.</span><span class="nx">apply</span><span class="p">(</span><span class="k">this</span><span class="p">,</span><span class="w"> </span><span class="nx">arguments</span><span class="p">);</span>
<span class="w"> </span><span class="p">};</span>
<span class="w"> </span><span class="c1">// Interception for "fetch" is different but the idea is the same</span>
<span class="w"> </span><span class="nb">window</span><span class="p">.</span><span class="nx">fetch</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">fetchReplacement</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span><span class="w"> </span><span class="nx">options</span><span class="p">,</span><span class="w"> </span><span class="p">...</span><span class="nx">rest</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">tid</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Math</span><span class="p">.</span><span class="nx">random</span><span class="p">().</span><span class="nx">toString</span><span class="p">(</span><span class="mf">36</span><span class="p">);</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">prom</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">oldFetch</span><span class="p">.</span><span class="nx">apply</span><span class="p">(</span><span class="k">this</span><span class="p">,</span><span class="w"> </span><span class="p">[</span><span class="nx">path</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="p">...</span><span class="nx">options</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="p">{},</span>
<span class="w"> </span><span class="nx">headers</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="p">...(</span><span class="nx">options</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="nx">options</span><span class="p">.</span><span class="nx">headers</span><span class="p">)</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="p">{},</span>
<span class="w"> </span><span class="p">[</span><span class="nx">wafkey</span><span class="p">]</span><span class="o">:</span><span class="w"> </span><span class="mf">1</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">...</span><span class="nx">rest</span><span class="p">]);</span>
<span class="w"> </span><span class="nx">fetchResults</span><span class="p">[</span><span class="nx">tid</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">arguments</span><span class="p">[</span><span class="mf">0</span><span class="p">];</span>
<span class="w"> </span><span class="nx">prom</span><span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="ow">delete</span><span class="w"> </span><span class="nx">fetchResults</span><span class="p">[</span><span class="nx">tid</span><span class="p">];</span>
<span class="w"> </span><span class="p">}).</span><span class="k">catch</span><span class="p">(</span><span class="kd">function</span><span class="w"> </span><span class="p">(</span><span class="nx">err</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="ow">delete</span><span class="w"> </span><span class="nx">fetchResults</span><span class="p">[</span><span class="nx">tid</span><span class="p">];</span>
<span class="w"> </span><span class="k">throw</span><span class="w"> </span><span class="nx">err</span><span class="p">;</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">prom</span><span class="p">;</span>
<span class="w"> </span><span class="p">};</span>
<span class="p">}</span>
<span class="k">export</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// We don't want to enable XHR interception in production so we should do it explicitly via</span>
<span class="w"> </span><span class="c1">// some mechanism. We chose a localStorage variable.</span>
<span class="w"> </span><span class="nx">init</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">initted</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">initted</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span><span class="p">;</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nx">localStorage</span><span class="w"> </span><span class="o">&&</span>
<span class="w"> </span><span class="nb">window</span><span class="p">.</span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">'iqe:chrome:init'</span><span class="p">)</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="s1">'true'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">wafkey</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">window</span><span class="p">.</span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">'iqe:wafkey'</span><span class="p">);</span>
<span class="w"> </span><span class="nx">init</span><span class="p">();</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="c1">// These variables we can read in Selenium using execute_script() method</span>
<span class="w"> </span><span class="nx">hasPendingAjax</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">xhrRemoved</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">xhrResults</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">result</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">result</span><span class="p">.</span><span class="nx">readyState</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="mf">4</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">result</span><span class="p">.</span><span class="nx">readyState</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="mf">0</span><span class="p">);</span>
<span class="w"> </span><span class="nx">xhrResults</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">xhrResults</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">result</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">result</span><span class="p">.</span><span class="nx">readyState</span><span class="w"> </span><span class="o">!==</span><span class="w"> </span><span class="mf">4</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="nx">result</span><span class="p">.</span><span class="nx">readyState</span><span class="w"> </span><span class="o">!==</span><span class="w"> </span><span class="mf">0</span><span class="p">);</span>
<span class="w"> </span><span class="nx">xhrRemoved</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">e</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`[iqe] xhr complete: </span><span class="si">${</span><span class="nx">e</span><span class="p">.</span><span class="nx">_url</span><span class="si">}</span><span class="sb">`</span><span class="p">));</span>
<span class="w"> </span><span class="nx">xhrResults</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">e</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`[iqe] xhr incomplete: </span><span class="si">${</span><span class="nx">e</span><span class="p">.</span><span class="nx">_url</span><span class="si">}</span><span class="sb">`</span><span class="p">));</span>
<span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">values</span><span class="p">(</span><span class="nx">fetchResults</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="nx">e</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`[iqe] fetch incomplete: </span><span class="si">${</span><span class="nx">e</span><span class="si">}</span><span class="sb">`</span><span class="p">));</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">xhrResults</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">></span><span class="w"> </span><span class="mf">0</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">fetchResults</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">></span><span class="w"> </span><span class="mf">0</span><span class="p">;</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nx">isPageSafe</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="o">!</span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">'[data-ouia-safe=false]'</span><span class="p">).</span><span class="nx">length</span><span class="w"> </span><span class="o">!==</span><span class="w"> </span><span class="mf">0</span><span class="p">,</span>
<span class="w"> </span><span class="nx">xhrResults</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">xhrResults</span><span class="p">;</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nx">fetchResults</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">fetchResults</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">};</span>
</code></pre></div>
<p>As you can see it's quite small piece of code and it allows us to know states of all XHR very
precisely. Moreover we don't depend on any framework because we overrode the primitives that are
used by all frameworks.</p>
<p>So what do we have in the end? It is possible to trace all javascript "activities" though it's a
quite complex task. We can trace animation and rendering via some indirect ways such as polling web
elements attributes. XHR requests tracing requires changing frontend code but it gives us the best
results. This gave us the idea that we could continue changing the frontend code to make developing
automation suite a bit easier and less hacky. My colleagues <a href="https://github.com/psav">Pete Savage</a>,
<a href="https://github.com/RonnyPfannschmidt">Ronny Pfannschmidt</a> and <a href="https://github.com/karelhala">Karel Hala</a>
created a specification for frontend developers that helps to solve aforementioned problems. The
specification is named <a href="https://ouia.readthedocs.io">Open UI Automation</a>. In the next post I'll
explain why it's cool and how it can help both frontend developers and testers.</p>Why I don't like Jenkins2020-06-02T00:00:00+02:002020-06-02T00:00:00+02:00tag:blog.misharov.pro,2020-06-02:/2020-06-02/why-i-dont-like-jenkinsI guess everybody knows Jenkins. It's one of the oldest and well-known automation system. It
was developed in the times when there was no Docker, Kubernetes, cloud native and other buzzwords. I
familiarized with it when I started to work at Red Hat. After a couple years of using I have some
concerns I want to share.<p>I guess everybody knows Jenkins. It's one of the oldest and well-known automation system. It
was developed in the times when there was no Docker, Kubernetes, cloud native and other buzzwords. I
familiarized with it when I started to work at Red Hat. After a couple years of using I have some
concerns I want to share.</p>
<h2>UI</h2>
<p>User interface it's a first thing that you see in software with GUI. Jenkins has archaic, slow and
unobvious user interface. If you have installed a bunch of plugins your settings page will become a
mess. In modern browsers some forms are just broken:</p>
<p><img class="image-center" alt="Jenkins Classic UI" src="https://blog.misharov.pro/assets/img/2020-06-02-why-i-dont-like-jenkins-1.png"/></p>
<p>There is more modern UI from Jenkins developers called Blue Ocean. It looks much nicer but it
doesn't have feature parity with the classic UI. So it can be used only for pipeline displaying.</p>
<p><img class="image-center" alt="Jenkins Blue Oсean UI" src="https://blog.misharov.pro/assets/img/2020-06-02-why-i-dont-like-jenkins-2.png"/></p>
<h2>Scalability</h2>
<p>Jenkins is distributed as one .war binary. It's supposed to be executed on some machine with java
installed. If you have a lot of jobs and users you are in trouble. Jenkins cannot scale. You will
end up spreading your jobs among several Jenkins instances.</p>
<h2>Pipeline syntax</h2>
<p>This one is controversial. Pipelines can described via declarative syntax or jenkins job DSL which
is built on top of Groovy.</p>
<h3>Declarative</h3>
<p>In my opinion declarative syntax looses to yaml based pipelines. It tends to have deep nested
structures which are hardly to read. Let's take an example from the official documentation:</p>
<div class="highlight"><pre><span></span><code>pipeline {
agent { docker 'maven:3-alpine' }
stages {
stage('Example Build') {
steps {
sh 'mvn -B clean verify'
}
}
}
}
</code></pre></div>
<p>You can see here is 4 nested parenthesis. Compare with possible Gitlab CI config:</p>
<div class="highlight"><pre><span></span><code><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">maven:3-alpine</span>
<span class="nt">stages</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">test</span>
<span class="nt">testing</span><span class="p">:</span>
<span class="w"> </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">tests</span>
<span class="w"> </span><span class="nt">script</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">mvn -B clean verify</span>
</code></pre></div>
<p>Don't get me wrong I'm not a yaml fan but in the most cases yaml looks more "declarative" than
Jenkins declarative syntax</p>
<h3>Scripted</h3>
<p>This may be the most powerful feature of Jenkins. You can implement any logic in your pipelines.
But in the end you will quickly discover that Groovy code which is executed somewhere in the Jenkins
guts almost not possible to debug. Really the only thing you can do is just put <code>println</code>. Therefore
take my advice do not try to implement any complex workflows. Make it as much simple as possible.</p>
<h2>Documentation</h2>
<p>I think Jenkins has bad documentation. At least everything that relates to pipeline. Not all aspects
covered in <a href="https://www.jenkins.io/doc/book/pipeline/">https://www.jenkins.io/doc/book/pipeline/</a> and often I need to search something in the
Internet.</p>
<p>I'm convinced that Jenkins is in the end of its lifecycle. The industry have changed but Jenkins
almost has not. Its plugin system extended its life but the retirement is coming. You can still use
it for small projects but highly recommend to look at its competitors if you start a new project.</p>The best Python debugger2020-05-24T00:00:00+02:002020-05-24T00:00:00+02:00tag:blog.misharov.pro,2020-05-24:/2020-05-24/best-python-debuggerDebugging is an essential part of programming. I guess you can agree with me that it takes
significant amount of time. Using an appropriate tool can save your time and efforts.<p>Debugging is an essential part of programming. I guess you can agree with me that it takes
significant amount of time. Using an appropriate tool can save your time and efforts.</p>
<h2>pdb</h2>
<p>This is a debugger that comes with Python standard library. This is the only advantage of it. Pdb
has quite limited functionality. You can set a break point inserting:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">pdb</span><span class="p">;</span> <span class="n">pdb</span><span class="o">.</span><span class="n">set_trace</span><span class="p">()</span>
</code></pre></div>
<p>Since Python 3.7 you can just use built-in function <code>breakpoint()</code>. There are commands that
recognized by pdb when you enter them in during debugging session.</p>
<p><a href="https://docs.python.org/3/library/pdb.html">Documentation</a></p>
<h2>PyCharm debugger</h2>
<p>PyCharm is very powerful tool and one of the best IDE for Python. It has a rich debugging features.
I'm not a PyCharm user and the reason why I don't use it is the requirement to "live" inside the
IDE. Yes, you can attach PyCharm debugger to remote python processes but it's not always
convenient.</p>
<p><a href="https://www.jetbrains.com/help/pycharm/debugging-code.html">Documentation</a></p>
<h2>pudb</h2>
<p>This is my choice. The quote from the docs:</p>
<blockquote>
<p>Its goal is to provide all the niceties of modern GUI-based debuggers in a more lightweight and
keyboard-friendly package. PuDB allows you to debug code right where you write and test it–in a
terminal.</p>
</blockquote>
<p>A breakpoint is set the following way:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">pudb</span><span class="p">;</span> <span class="n">pu</span><span class="o">.</span><span class="n">db</span>
</code></pre></div>
<p>If you are using Python 3.7 or newer, you can specify <code>PYTHONBREAKPOINT</code> environment variable and
built-in <code>breakpoint()</code> will call <code>pudb</code>:</p>
<div class="highlight"><pre><span></span><code># Set breakpoint() in Python to call pudb
export PYTHONBREAKPOINT="pudb.set_trace"
</code></pre></div>
<p>It has two killer features. It's a nice pseudo graphical UI and ability to switch to a custom shell
during a debugging session. For example you can open an <code>ipython</code> shell by pressing <code>!</code>. It opens
vast opportunities for code inspecting and examining your assumptions in the exactly same conditions
where your code fails. Of course you move up and down through the stack, set new breakpoints and
execute the code line by line and other standard debugging features.</p>
<p>{% picture assets/img/2020-05-24-best-python-debugger-1.png --alt pudb screenshot %}</p>
<h3>pytest integration</h3>
<p>Pytest is one of my main testing frameworks and it's very nice that pudb has integration with it.
There is a pytest plugin <code>pytest-pudb</code> that can stop the execution post-mortem. Let's say your test
failed and you would like to find out a reason. So just install <code>pytest-pudb</code> package and append
<code>--pudb</code> to pytest command:</p>
<div class="highlight"><pre><span></span><code>pytest<span class="w"> </span>--pudb
</code></pre></div>
<p>I often use this plugin and find it very helpful.</p>
<h2>References</h2>
<p><a href="https://documen.tician.de/pudb/">Documentation</a></p>My favorite distro2020-05-16T00:00:00+02:002020-05-16T00:00:00+02:00tag:blog.misharov.pro,2020-05-16:/2020-05-16/my-favorite-distroI've been using Linux for 11 years already. I've tried several Linux distribution in both desktop
and server use cases. I wouldn't call myself a distrohopper but I've collected some observations
about some popular Linux distributions.<p>I've been using Linux for 11 years already. I've tried several Linux distribution in both desktop
and server use cases. I wouldn't call myself a distrohopper but I've collected some observations
about some popular Linux distributions.</p>
<h2><a href="https://ubuntu.com">Ubuntu</a></h2>
<p>It was my first Linux distribution. I had both Windows and Ubuntu installed on my desktop in dual
boot. I started with Ubuntu 9.10 and learned some basic things such as software repositories,
package managers, terminal and others.</p>
<h2><a href="https://elementary.io">Elementary</a></h2>
<p>I'm not a hardcore "terminal-only" guy. I care about look and feel of an operating system. So when
I saw screenshots of Elementary OS I wanted to give it a chance and try to work with it more
closely. It has a beautiful design and it looks like Elementary developers are inspired by Mac OS.
Unfortunately it was quite unstable and updates were irregular.</p>
<h2><a href="https://www.opensuse.org/">OpenSUSE</a></h2>
<p>I chose OpenSUSE because I wanted to switch from Ubuntu's Unity to KDE. I used both editions
Leap and Tumbleweed. This distribution provides a rock solid base with some cool features such as
YAST, snapshots and one of the best installers. In my opinion it's a bit bloated but despite on that
openSUSE Tumbleweed is my main Linux distro on my home desktop machines for more than 5 years.</p>
<h2><a href="https://www.redhat.com/en/technologies/linux-platforms/enterprise-linux">RHEL</a></h2>
<p>I've never used neither CentOS or RHEL as a desktop system but in my work at Red Hat RHEL is a main
operating system in the company's infrastructure (who would have thought 🙂). If you need support
and enterprise software I believe it's the only choice.</p>
<h2><a href="https://neon.kde.org/">KDE Neon</a></h2>
<p>Since I tried first time KDE it's the only DE I use on my desktop machines. Neon is an official
distro from KDE developers based on Ubuntu LTS with the latest stable KDE packages. Updates arrive
very often but I haven't faced any major issues. When I got a new laptop at work I installed Neon
on it and I've been using it for 3 years already.</p>
<p>So I have actually two favorite Linux distributions: KDE Neon and OpenSUSE Tumbleweed. Both of them
have the latest KDE. For work I need more stable base so I use Neon. At home I want to have fully
rolling release and OpenSUSE Tumbleweed is still a good choice.</p>Python retry!2020-05-12T00:00:00+02:002020-05-12T00:00:00+02:00tag:blog.misharov.pro,2020-05-12:/2020-05-12/python-retryIn programming it's quite often you need to wait for an action to complete or some service
availability. There are many ways and tools which able to do that. I would like to tell about two
python libraries I've worked with.<p>In programming it's quite often you need to wait for an action to complete or some service
availability. There are many ways and tools which able to do that. I would like to tell about two
python libraries I've worked with.</p>
<h2><a href="https://github.com/RedHatQE/wait_for">wait_for</a></h2>
<p>It's a small library originally created by my colleague Pete Savage
<a href="https://github.com/psav">@psav</a>. <code>wait_for</code> is heavily used in web UI tests. When you click some
button you not always get the result immediately. Request processing can take some time. Some
application will show the result after page refresh, more modern applications use XHR and tons of
scripts 🤑 to update the page. In both cases you cannot just perform button clicking in the python
code. You have to wait until the result appears and only after that continue execution. Let me
provide some examples of usage:</p>
<div class="highlight"><pre><span></span><code><span class="n">view</span><span class="o">.</span><span class="n">submit_button</span><span class="o">.</span><span class="n">click</span><span class="p">()</span>
<span class="n">wait_for</span><span class="p">(</span>
<span class="k">lambda</span><span class="p">:</span> <span class="n">view</span><span class="o">.</span><span class="n">flash</span><span class="o">.</span><span class="n">is_displayed</span><span class="p">,</span>
<span class="n">delay</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span>
<span class="n">timeout</span><span class="o">=</span><span class="mi">120</span>
<span class="p">)</span>
<span class="n">view</span><span class="o">.</span><span class="n">flash</span><span class="o">.</span><span class="n">assert_no_error</span><span class="p">()</span>
</code></pre></div>
<p>Here test automation clicks a button and then wait for another element appearing in the UI.
<code>wait_for</code> just executes <code>lambda: view.flash.is_displayed</code> over and over again until result will be
<code>True</code> or time out.</p>
<p><code>wait_for</code> has a nice decorator:</p>
<div class="highlight"><pre><span></span><code><span class="nd">@wait_for_decorator</span><span class="p">(</span><span class="n">delay</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">300</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">volume_type_is_displayed</span><span class="p">():</span>
<span class="n">volume_type</span><span class="o">.</span><span class="n">refresh</span><span class="p">()</span>
<span class="k">return</span> <span class="n">volume_type</span><span class="o">.</span><span class="n">exists</span>
</code></pre></div>
<h2><a href="https://github.com/jd/tenacity">tenacity</a></h2>
<p>Everything gets a lot more fun when you start to use <code>asyncio</code>. First of all you'll find out that
<code>time.sleep()</code> blocks the main thread and you cannot use it in your asynchronous code. But wait
<code>wait_for</code> uses <code>time.sleep()</code>
<a href="https://github.com/RedHatQE/wait_for/blob/master/wait_for/__init__.py#L192">underneath</a>. I wanted
to add <code>asyncio</code> support but before I started I decided to search if someone already did it (yeah
I'm lazy I know). This is how I discovered <code>tenacity</code> (credits to
<a href="https://julien.danjou.info/">Julien Danjou</a>). You can use it in both synchronous and
asynchronous code under the same API. My task was to upload a payload to a server and wait for the
result from some REST API endpoint. But I wanted to upload a lot of payloads and check the results
after that. <code>tenacity</code> and <code>asyncio</code> fit perfectly for that task. Here is an example:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">asyncio</span>
<span class="kn">from</span> <span class="nn">tenacity</span> <span class="kn">import</span> <span class="n">retry</span>
<span class="kn">from</span> <span class="nn">tenacity</span> <span class="kn">import</span> <span class="n">retry_if_exception_type</span>
<span class="kn">from</span> <span class="nn">tenacity</span> <span class="kn">import</span> <span class="n">retry_if_result</span>
<span class="kn">from</span> <span class="nn">tenacity</span> <span class="kn">import</span> <span class="n">stop_after_delay</span>
<span class="kn">from</span> <span class="nn">tenacity</span> <span class="kn">import</span> <span class="n">wait_fixed</span>
<span class="nd">@retry</span><span class="p">(</span>
<span class="n">stop</span><span class="o">=</span><span class="n">stop_after_delay</span><span class="p">(</span><span class="mi">300</span><span class="p">),</span>
<span class="n">wait</span><span class="o">=</span><span class="n">wait_fixed</span><span class="p">(</span><span class="mi">4</span><span class="p">),</span>
<span class="n">retry_error_callback</span><span class="o">=</span><span class="k">lambda</span> <span class="n">retry_state</span><span class="p">:</span> <span class="kc">False</span><span class="p">,</span>
<span class="n">retry</span><span class="o">=</span><span class="p">(</span><span class="n">retry_if_result</span><span class="p">(</span><span class="k">lambda</span> <span class="n">value</span><span class="p">:</span> <span class="n">value</span> <span class="ow">is</span> <span class="kc">False</span><span class="p">)</span> <span class="o">|</span> <span class="n">retry_if_exception_type</span><span class="p">(</span><span class="ne">Exception</span><span class="p">)),</span>
<span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">find_host</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">hostname</span><span class="p">):</span>
<span class="n">url</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"https://example.com/hostnames/</span><span class="si">{</span><span class="n">hostname</span><span class="si">}</span><span class="s2">"</span>
<span class="n">resp</span> <span class="o">=</span> <span class="k">await</span> <span class="n">session</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
<span class="k">if</span> <span class="n">resp</span><span class="o">.</span><span class="n">status</span> <span class="o">!=</span> <span class="mi">200</span>
<span class="k">return</span> <span class="kc">False</span>
<span class="n">resp_json</span> <span class="o">=</span> <span class="k">await</span> <span class="n">resp</span><span class="o">.</span><span class="n">json</span><span class="p">()</span>
<span class="k">return</span> <span class="nb">bool</span><span class="p">(</span><span class="n">resp_json</span><span class="p">[</span><span class="s2">"data"</span><span class="p">])</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">upload_payload</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">hostname</span><span class="p">):</span>
<span class="n">data</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"some_payload_with_</span><span class="si">{</span><span class="n">hostname</span><span class="si">}</span><span class="s2">"</span>
<span class="n">resp</span> <span class="o">=</span> <span class="k">await</span> <span class="n">session</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="s2">"https://example.com"</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">data</span><span class="p">)</span>
<span class="n">is_host_found</span> <span class="o">=</span> <span class="k">await</span> <span class="n">find_host</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">hostname</span><span class="p">)</span>
<span class="n">message</span> <span class="o">=</span> <span class="s2">"host </span><span class="si">%s</span><span class="s2"> was found!"</span> <span class="k">if</span> <span class="n">is_host_found</span> <span class="k">else</span> <span class="s2">"host </span><span class="si">%s</span><span class="s2"> wasn't found in time"</span>
<span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="n">hostname</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">scheduler</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">base_hostname</span><span class="p">,</span> <span class="n">num_uploads</span><span class="p">):</span>
<span class="n">tasks</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">num_uploads</span><span class="p">):</span>
<span class="n">hostname</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s2">.</span><span class="si">{</span><span class="n">base_hostname</span><span class="si">}</span><span class="s2">"</span>
<span class="n">task</span> <span class="o">=</span> <span class="n">upload_payload</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">hostname</span><span class="p">)</span>
<span class="n">task</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">ensure_future</span><span class="p">(</span><span class="n">task</span><span class="p">)</span>
<span class="n">tasks</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">task</span><span class="p">)</span>
<span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">wait</span><span class="p">(</span><span class="n">tasks</span><span class="p">)</span>
<span class="k">await</span> <span class="n">session</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</code></pre></div>
<p>What's happening in this piece of code?</p>
<ol>
<li><code>scheduler</code> creates number of tasks <code>upload_payload</code> equals to <code>num_uploads</code>.</li>
<li><code>upload_payload</code> uploads a payload ans waits for the result in <code>find_host</code></li>
<li><code>find_host</code> every 4 seconds checks <code>f"https://example.com/hostnames/{hostname}"</code> endpoint and
returns <code>True</code> if it's found. Otherwise it fails with timeout after 5 minutes.</li>
</ol>
<h2>Conclusion</h2>
<p><code>tenacity</code> is a powerful library and if you need to retry operations in asynchronous code it's the
best choice. In other cases <code>wait_for</code> will be more than enough. In my opinion it has simpler
API and doesn't force you to decorate functions.</p>Gitlab runner in Openshift2020-05-08T00:00:00+02:002020-05-08T00:00:00+02:00tag:blog.misharov.pro,2020-05-08:/2020-05-08/gitlab-runner-in-openshiftI'm pretty sure most of you familiar with Gitlab. It has a lot of features and nowadays it looks a
bit bloated but one thing I really love in this tool. It's Gitlab CI. One of the coolest thing there
is it can run jobs on many platforms including virtual machines, Docker containers and Kubernetes.<p>I'm pretty sure most of you familiar with Gitlab. It has a lot of features and nowadays it looks a
bit bloated but one thing I really love in this tool. It's Gitlab CI. One of the coolest thing there
is it can run jobs on many platforms including virtual machines, Docker containers and Kubernetes.</p>
<h2>Gitlab CI</h2>
<p>Let's look in detail how Gitlab CI works and configured.</p>
<p><strong>Gitlab runner</strong> is an agent which communicates with Gitlab server, receives tasks from it and
executes them on a selected executor. You can install the runner on any linux host or put it into a
container.</p>
<p><strong>Executor</strong> is an environment where the code will be ran. Here is a list of available executors:
SSH, Shell, VirtualBox, Parallels, Docker, Kubernetes and Custom.</p>
<p>I would like to pay more attention to Kubernetes executor. When Gitlab runner is configured to use
this executor it spawns pods in a Kubernetes cluster and jobs from pipelines are executed inside the
pods. You can install a runner to your cluster via Gitlab web UI just clicking one button. But it
assumes that you have cluster admin privileges. Moreover if you have an Openshift cluster you should
take in account some its "features".</p>
<h2>Openshift</h2>
<p>Now let's talk about Openshift. It's a Kubernetes distribution which is developed by Red Hat. It has
more advanced security policies and provides some custom objects such as DeploymentConfigs,
BuildConfigs, ImageStreams and others. Openshift has also Templates API. It's something similar to
Helm charts but less flexible and only Openshift specific. Nevertheless Openshift templates are a
convenient way to pack all objects which are related to one service in one YAML file.</p>
<h2>Gitlab runner template</h2>
<p>So I decided to create such <a href="https://github.com/RedHatQE/ocp-gitlab-runner">template</a> for Gitlab
runner. Let me explain what objects are created and a deployment workflow.</p>
<ol>
<li>The following objects are created:<ul>
<li><code>gitlab-runner-config-map</code> ConfigMap;</li>
<li><code>gitlab</code> ServiceAccount;</li>
<li><code>gitlab-rb</code> RoleBinding for <code>gitlab</code> ServiceAccount;</li>
<li><code>gitlab-runner</code> BuildConfig;</li>
<li><code>gitlab-runner</code> ImageStream;</li>
<li><code>gitlab-helper</code> BuildConfig;</li>
<li><code>gitlab-helper</code> ImageStream;</li>
<li><code>gitlab-runner</code> DeploymentConfig.</li>
</ul>
</li>
<li>When image is changed in <code>gitlab-runner</code> ImageStream <code>gitlab-runner</code> DeploymentConfig is started.</li>
<li>Two init containers are spawned:<ul>
<li><code>busybox</code> copies <code>config.toml</code> from <code>gitlab-runner-config-map</code> ConfigMap to a shared volume
named <code>data</code>;</li>
<li><code>gitlab-runner-init</code> container registers a runner for a given Gitlab instance and changes
<code>config.toml</code> in the shared volume <code>data</code>.</li>
</ul>
</li>
<li>Finally a pod with <code>gitlab-runner</code> container starts with <code>config.toml</code> which is mounted from the
shared volume <code>data</code>.</li>
</ol>
<p>After that you will see a new entry in the UI under <code>Settings</code>/<code>CI / CD</code>/<code>Runners</code>.</p>
<h2>Usage</h2>
<p>There are two ways how you can apply this template: via web UI or CLI. In the UI you just need to
copy and paste the content of <code>ocp-gitlab-runner-template.yaml</code> into <code>Import YAML</code> dialog. Then
instantiate the template in order to create the objects. For command line interface you should use
<code>oc</code> utility:</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span>oc<span class="w"> </span>process<span class="w"> </span>-f<span class="w"> </span>ocp-gitlab-runner-template.yaml<span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-p<span class="w"> </span><span class="nv">NAME</span><span class="o">=</span><span class="s2">"gitlab-runner"</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-p<span class="w"> </span><span class="nv">CI_SERVER_URL</span><span class="o">=</span><span class="s2">"URL of a Gitlab REST API"</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-p<span class="w"> </span><span class="nv">REGISTRATION_TOKEN</span><span class="o">=</span><span class="s2">"Runner's registration token"</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-p<span class="w"> </span><span class="nv">CONCURRENT</span><span class="o">=</span><span class="s2">"The maximum number of concurrent CI pods"</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span><span class="p">|</span><span class="w"> </span>oc<span class="w"> </span>create<span class="w"> </span>-f<span class="w"> </span>-
</code></pre></div>
<p>That's it. I hope someone find it useful and feel free to use it.</p>
<h2>References</h2>
<ul>
<li><a href="https://gitlab.com">Gitlab</a></li>
<li><a href="https://www.openshift.com/">Openshift</a></li>
<li><a href="https://docs.gitlab.com/ee/ci/">GitLab CI/CD docs</a></li>
<li><a href="https://docs.openshift.com/container-platform/4.4/openshift_images/using-templates.html">Openshift templates docs</a></li>
<li><a href="https://github.com/RedHatQE/ocp-gitlab-runner">OCP Gitlab runner repository</a></li>
<li><a href="https://helm.sh/">Helm</a></li>
</ul>UI testing in Kubernetes2020-05-02T00:00:00+02:002020-05-02T00:00:00+02:00tag:blog.misharov.pro,2020-05-02:/2020-05-02/ui-testing-in-kubernetesAs a quality engineer I responsible for testing various parts of a product. One of such part can be
the web UI. But before your first UI test will be executed you will need to configure a lot of
things. It might be a non-trivial task. Kubernetes can be quite helpful here and be actual
"one ring to rule them all".<p>As a quality engineer I responsible for testing various parts of a product. One of such part can be
the web UI. But before your first UI test will be executed you will need to configure a lot of
things. It might be a non-trivial task. Kubernetes can be quite helpful here and be actual
"one ring to rule them all".</p>
<h2>High-level overview</h2>
<p>Let's consider a typical setup of UI testing with Jenkins as a CI system:</p>
<p><img class="image-center" alt="diagram 1" src="https://blog.misharov.pro/assets/img/2020-05-02-ui-testing-in-kubernetes-1.png"/></p>
<ol>
<li>Jenkins server initializes a test session and sends the job configuration to a jenkins agent.</li>
<li>Jenkins agent executes shell commands specified in the job.</li>
<li>These commands usually run tests from some tests suite.</li>
<li>When a UI test starts it communicates with a selenium server in order to perform actions in the
browser.</li>
<li>Selenium server sends commands to a specific browser driver and then browser open urls, click
buttons and so on.</li>
<li>If you want to observe how your test is running you can configure a VNC server and connect to it
with any VNC client.</li>
</ol>
<h2>Selenium and containers</h2>
<p>You can run selenium in a standalone mode on your machine but that setup has some disadvantages. For
example it's quite problematically to have installed several versions of a browser. Docker brings
a significant improvement into web UI testing. Now you can have various images with required browser
versions. Besides, containers don't persist the state. It means every test session starts in the
same conditions and doesn't affect on next ones.</p>
<h2>Jenkins and Kubernetes</h2>
<p>Nowadays more and more routines are adopted to be run in Kubernetes and various CI systems are not
an exception. I've already mentioned Jenkins here so let's continue talk about that automation
server. Jenkins has <a href="https://github.com/jenkinsci/kubernetes-plugin">kubernetes plugin</a> that
dynamically spawns agents in a kubernetes cluster. This approach may give us some new ideas about
how to run tests. As you may know kubernetes operates
<a href="https://kubernetes.io/docs/concepts/workloads/pods/pod/">pods</a>. Think of it as a group of
containers with some shared resources such as network and mounts. So let's combine all pieces
together:</p>
<p><img class="image-center" alt="diagram 2" src="https://blog.misharov.pro/assets/img/2020-05-02-ui-testing-in-kubernetes-2.png"/></p>
<p>We can describe this setup in a <code>podTemplate</code> of jenkins job DSL:</p>
<div class="highlight"><pre><span></span><code><span class="n">podTemplate</span><span class="o">(</span><span class="nl">containers:</span><span class="w"> </span><span class="o">[</span>
<span class="w"> </span><span class="n">containerTemplate</span><span class="o">(</span>
<span class="w"> </span><span class="nl">name:</span><span class="w"> </span><span class="s2">"jenkins-agent"</span><span class="o">,</span>
<span class="w"> </span><span class="nl">image:</span><span class="w"> </span><span class="s2">"jenkins/jnlp-slave"</span><span class="o">,</span>
<span class="w"> </span><span class="nl">args:</span><span class="w"> </span><span class="s1">'${computer.jnlpmac} ${computer.name}'</span>
<span class="w"> </span><span class="o">),</span>
<span class="w"> </span><span class="n">containerTemplate</span><span class="o">(</span>
<span class="w"> </span><span class="nl">name:</span><span class="w"> </span><span class="s2">"tests-suite"</span><span class="o">,</span>
<span class="w"> </span><span class="nl">image:</span><span class="w"> </span><span class="s2">"some_tests_suite_image"</span><span class="o">,</span>
<span class="w"> </span><span class="nl">ttyEnabled:</span><span class="w"> </span><span class="kc">true</span><span class="o">,</span>
<span class="w"> </span><span class="nl">command:</span><span class="w"> </span><span class="s2">"cat"</span>
<span class="w"> </span><span class="o">),</span>
<span class="w"> </span><span class="n">containerTemplate</span><span class="o">(</span>
<span class="w"> </span><span class="nl">name:</span><span class="w"> </span><span class="s2">"selenium"</span><span class="o">,</span>
<span class="w"> </span><span class="nl">image:</span><span class="w"> </span><span class="s2">"selenium/standalone-firefox-debug"</span>
<span class="w"> </span><span class="o">)</span>
<span class="o">])</span><span class="w"> </span><span class="o">{</span>
<span class="w"> </span><span class="n">node</span><span class="o">(</span><span class="n">POD_LABEL</span><span class="o">)</span><span class="w"> </span><span class="o">{</span>
<span class="w"> </span><span class="o">...</span>
<span class="w"> </span><span class="o">}</span>
<span class="o">}</span>
</code></pre></div>
<p>or via more familiar YAML:</p>
<div class="highlight"><pre><span></span><code><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">v1</span>
<span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">Pod</span>
<span class="nt">metadata</span><span class="p">:</span>
<span class="w"> </span><span class="nt">labels</span><span class="p">:</span>
<span class="w"> </span><span class="nt">some-label</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">some-label-value</span>
<span class="nt">spec</span><span class="p">:</span>
<span class="w"> </span><span class="nt">containers</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">jenkins-agent</span>
<span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">jenkins/jnlp-slave</span>
<span class="w"> </span><span class="nt">args</span><span class="p">:</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">'\$(JENKINS_SECRET)'</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="s">'\$(JENKINS_NAME)'</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">tests-suite</span>
<span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">some_tests_suite_image</span>
<span class="w"> </span><span class="nt">tty</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">true</span>
<span class="w"> </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p p-Indicator">[</span><span class="s">"cat"</span><span class="p p-Indicator">]</span>
<span class="w"> </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">selenium</span>
<span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">selenium/standalone-firefox-debug</span>
</code></pre></div>
<p>Jenkins Kubernetes plugin and this pod configuration can be in some way an alternative of
<a href="https://www.selenium.dev/documentation/en/grid/">Selenium Grid</a>. There is a one notable moment
about Jenkins jobs. They can be
<a href="https://www.jenkins.io/doc/pipeline/examples/#parallel-multiple-nodes">parallelized</a> across
multiple Jenkins nodes. In that case each node will have its own selenium instance.</p>
<p>Pros:</p>
<ul>
<li>you don't need to setup and maintain complex Selenium Grid architecture;</li>
<li>the latency is minimal because both tests suite and selenium containers are running in the same
pod;</li>
</ul>
<p>Cons:</p>
<ul>
<li>you have to implement a job paralellizer in jenkins dsl;</li>
<li>you lose Selenium Grid dashboard;</li>
</ul>
<h2>Summary</h2>
<p>As you may see Kubernetes and containers are game changers. They give you more room to maneuver.
In this post I described one possible scenario of the usage but there are other attempts to adapt
Selenium to run in Kubernetes such as <a href="https://aerokube.com/moon/latest/">Moon</a>,
<a href="https://opensource.zalando.com/zalenium/">Zalenium</a> and
<a href="https://github.com/wrike/callisto">Callisto</a>.</p>Xakac is a payload forwarder2020-04-30T00:00:00+02:002020-04-30T00:00:00+02:00tag:blog.misharov.pro,2020-04-30:/2020-04-30/xakacOnce upon a time I was need to configure a CI for a private github repo. Public CI providers
such as Travis, CircleCI and other don't provide free plans for private repos. Besides tests I
wanted to run are not publicly available. So I decided to provision a Jenkins server inside
corporate VPN. How to trigger a pipeline on PR? Github provides a webhooks mechanism. Generally
speaking it's a just HTTP request with some payload. And here we have a problem. How to pass a
github webhook to the Jenkins which is not exposed to the Internet?<p>Once upon a time I was need to configure a CI for a private github repo. Public CI providers
such as Travis, CircleCI and other don't provide free plans for private repos. Besides tests I
wanted to run are not publicly available. So I decided to provision a Jenkins server inside
corporate VPN. How to trigger a pipeline on PR? Github provides a webhooks mechanism. Generally
speaking it's a just HTTP request with some payload. And here we have a problem. How to pass a
github webhook to the Jenkins which is not exposed to the Internet?</p>
<h2>Possible solutions</h2>
<ol>
<li>
<p>The dumbest solution would be just not use webhooks. Jenkins can be configured to poll a certain
repository to check if there are some new PRs.</p>
</li>
<li>
<p>More elegant way is somehow to pass webhooks inside corporate VPN.</p>
</li>
</ol>
<p>I'm not keen on polling and prefer "push" over it. I started to search if there are some services
that solve my problem. As I expected I'm not the only one with such problem and some companies have
implemented so-called webhook forwarders. I found at least three such services:</p>
<ul>
<li><a href="http://smee.io">smee.io</a></li>
<li><a href="https://ngrok.com/">ngrok.com</a></li>
<li><a href="https://webhookrelay.com/">webhookrelay.com</a></li>
</ul>
<p>One thing I should mention that Jenkins itself cannot connect to a webhook forwarder. There should
be one more piece in the route. All these services also provides clients that connect to the server
and then replay webhooks inside corporate network. Consider the following diagram:</p>
<p><img class="image-center" alt="diagram 1" src="https://blog.misharov.pro/assets/img/2020-04-30-xakac-1.png"></p>
<p>I chose smee.io because both server and client are free and open source unlike other two. I
created a route in smee.io and provisioned their client in our Openshift instance. It looked that
the problem was solved.</p>
<h2>Inconveniences</h2>
<p>I was happy until I was need to create another webhook forwarder route. The issue is smee.io client
can forward webhooks only between one source and destination pair. It means that for every new
webhook I would need to provision a new smee client. That looks a bit overhead for such simple task.
Moreover the client is written in javascript :). Not a big deal but again for such simple task using
this programming language is suboptimal.</p>
<p>I decided to write my own smee.io client implementation with two key features:</p>
<ol>
<li>
<p>It must be distributed as a single binary without any dependencies.</p>
</li>
<li>
<p>One running instance of the client must forward multiple source-destination pairs.</p>
</li>
</ol>
<h2>Enter Xakac</h2>
<p>This is how I came up to idea to create <code>xakac</code>. A small utility written on golang that replays
webhooks which were sent by smee.io server. Golang perfectly fits to this task. Each
source-destination pair works in a separate goroutine and source code is compiling into one small
executable file without dependencies.</p>
<p>You may ask what does xakac mean? It's a transliteration of russian word хакас
(<a href="https://en.wikipedia.org/wiki/Khakas_people">khakas</a>). It's native people of the place where I
came from. By the way you can find the code here:</p>
<p><a href="https://github.com/quarckster/xakac">https://github.com/quarckster/xakac</a></p>
<h2>Known issues</h2>
<p>It was a good exercise for me and my <code>xakac</code> works fine except one thing. Sometimes connection
between client and server can suddenly be closed and in the logs I can see this error:</p>
<div class="highlight"><pre><span></span><code>unexpected EOF
</code></pre></div>
<p>I need to figure out how to handle this situation and automatically reconnect a failed route.</p>