1
0

Compare commits

...

15 Commits

Author SHA1 Message Date
8c9da1d33f Fix openQA Helm Chart installation
All checks were successful
Build / build (push) Successful in 19s
2025-06-19 14:39:36 +02:00
c311b67b04 Installing openQA on Kubernetes with Helm Charts
All checks were successful
Build / build (push) Successful in 19s
2025-06-04 13:40:32 +02:00
93c77ebd48 Improve Gitea Action example
All checks were successful
Build / build (push) Successful in 20s
2025-06-03 15:01:46 +02:00
ad3ba54726 Enable automatic build and deployment
All checks were successful
Build / build (push) Successful in 14s
2025-06-03 13:43:13 +02:00
3085297dbe Fix minor typo 2025-06-03 12:51:03 +02:00
9c15c4d4d4 Obfuscate e-mail address 2025-05-19 17:07:54 +02:00
e3dcd4c6b0 Enable smooth scrolling 2025-05-15 21:07:31 +02:00
0f6cc9b1fc Fix indentation 2025-05-15 21:03:52 +02:00
e7d373f886 Display TOC in sidebar 2025-05-15 21:02:19 +02:00
b851a127c7 Improve search
This new approach makes sure the input field is hidden on browsers which
ignore `display:none`, such as w3m or lynx.
2025-05-14 14:51:49 +02:00
a2b355477f Add profile picture 2025-05-06 11:33:53 +02:00
6b245f540d Better mobile nav 2025-05-05 19:02:42 +02:00
85f0e72676 Remove mobile-specific navigation options 2025-05-05 18:53:50 +02:00
ee77ff7c84 Add education in about.md 2025-02-11 21:39:43 +01:00
f0571e9e38 Change event listener init 2025-01-10 09:49:43 +01:00
14 changed files with 304 additions and 54 deletions

View File

@ -0,0 +1,38 @@
name: Build
on:
push:
branches:
- master
env:
ZOLA_VERSION: ${{ vars.ZOLA_VERSION }}
HOST: ${{ vars.HOST }}
HOST_DIR: ${{ vars.HOST_DIR }}
SSH_USERNAME: ${{ secrets.SSH_USERNAME }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Zola
run: |
wget https://github.com/getzola/zola/releases/download/v${ZOLA_VERSION}/zola-v${ZOLA_VERSION}-x86_64-unknown-linux-gnu.tar.gz
tar -xvzf *.tar.gz
- name: Build
run: ./zola build
- name: Deploy
run: |
apt update -y && apt-get install -y --no-install-recommends rsync
eval "$(ssh-agent -s)"
ssh-add - <<< "${SSH_PRIVATE_KEY}"
mkdir -p ~/.ssh/
ssh-keyscan -H ${HOST} >> ~/.ssh/known_hosts
rsync -r --delete-after public/* "${SSH_USERNAME}@${HOST}:${HOST_DIR}"

View File

@ -21,7 +21,6 @@ nav = [
{title = "Index", path = "/"}, {title = "Index", path = "/"},
{title = "About", path = "/about/"}, {title = "About", path = "/about/"},
{title = "Search", path = "/posts/"}, {title = "Search", path = "/posts/"},
{title = "Categories", path = "/categories/", mobile_only = true}
] ]
latest_posts_count = 3 latest_posts_count = 3
repository_url = "https://git.0x45.cz/em/em.0x45.cz" repository_url = "https://git.0x45.cz/em/em.0x45.cz"

View File

@ -4,13 +4,15 @@ title = "About"
notoc = true notoc = true
+++ +++
This is my personal website mainly used for technical articles about problems and projects I am working on. <img src="profile-picture.jpg" alt="Profile Picture" class="profile-picture">
This is my personal website mainly used for technical articles about problems and projects I&nbsp;am working on.
It is generated to static HTML via [Zola](https://www.getzola.org/). It is generated to static HTML via [Zola](https://www.getzola.org/).
## Contact me ## Contact me
- [em@0x45.cz](mailto:em@0x45.cz) - [&#101;&#109;&#64;&#48;&#120;&#52;&#53;&#46;&#99;&#122;](&#109;&#97;&#105;&#108;&#116;&#111;&#58;&#101;&#109;&#64;&#48;&#120;&#52;&#53;&#46;&#99;&#122;)
- [@irungentoo](https://t.me/irungentoo) on Telegram - [@irungentoo](https://t.me/irungentoo) on Telegram
- PGP: [0x453A7AE1754BFED2](https://keys.openpgp.org/vks/v1/by-fingerprint/3B08B7B5F00CCB0370EE3E71453A7AE1754BFED2) - PGP: [0x453A7AE1754BFED2](https://keys.openpgp.org/vks/v1/by-fingerprint/3B08B7B5F00CCB0370EE3E71453A7AE1754BFED2)
@ -18,9 +20,23 @@ It is generated to static HTML via [Zola](https://www.getzola.org/).
My primary employment is at [SUSE](https://www.suse.com) as a security QA engineer. I also work (or have worked) as a teacher at several Prague schools, mainly at [Gymnázium Jana Keplera](https://gjk.cz/). My primary employment is at [SUSE](https://www.suse.com) as a security QA engineer. I also work (or have worked) as a teacher at several Prague schools, mainly at [Gymnázium Jana Keplera](https://gjk.cz/).
## Education
Both of my degrees have been obtained from the Faculty of Education at Charles University in Prague, specializing in computer science and education.
My Bachelor's thesis explores static website generators and includes a reference implementation using Zola.
- [Bachelor's Thesis Text](https://dspace.cuni.cz/bitstream/handle/20.500.11956/152842/130279410.pdf)
- [Defense Results](https://dspace.cuni.cz/handle/20.500.11956/152842)
Master's thesis focuses on implementing a custom ESP32 development board expandable by custom extension boards, along with a set educational materials which can be applied when teaching embedded programing. Both source files and educational materials can be found at [capyboard.dev](https://capyboard.dev) or at [my GitHub profile](https://github.com/realcharmer).
- [Master's Thesis Text](https://dspace.cuni.cz/bitstream/handle/20.500.11956/196503/120496276.pdf)
- [Defense Results](https://dspace.cuni.cz/handle/20.500.11956/196503)
## Projects ## Projects
Some of my project are published directly at this website or in Git: Some of my projects are published directly at this website or in Git:
- [git.0x45.cz/em](https://git.0x45.cz/em) - [git.0x45.cz/em](https://git.0x45.cz/em)
- [git.microlab.space/em](https://git.microlab.space/em) - [git.microlab.space/em](https://git.microlab.space/em)

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,127 @@
+++
title = "Installing openQA on Kubernetes with Helm Charts"
date = 2025-06-04
[taxonomies]
categories = ["Linux"]
[extra]
author = "Emil Miler"
+++
Recently, I experimented with Kubernetes by installing [openQA](https://open.qa/) using Helm Charts. This article is a simple guide on how to do the same locally on your computer, with minimal effort.
<!-- more -->
You need a working cluster. There are several single-node management tools that let you run a Kubernetes cluster locally on your machine, such as [k3d](https://k3d.io/), [kind](https://kind.sigs.k8s.io/), or [minikube](https://minikube.sigs.k8s.io/). They provide a cluster entirely isolated within a container.
I will be using minikube.
## Installation of Tools
Install minikube -- or any other management tool -- along with *helm*. For example, on Void Linux:
```
xbps-install -S minikube kubernetes-helm
```
You also have to have a running container engine, such as [Podman](https://podman.io/) or [Docker](https://www.docker.com/). Configuring a container engine is outside the scope of this article.
## Preparing the Cluster
Since I am using minikube, I can simply start it like so:
```
minikube start
```
This might take a while during the initial setup. If you want, you can also run a web-based dashboard for better visualization and debugging.
```
minikube dashboard
```
The dashboard will open in your default browser.
## Downloading openQA Charts
The official charts are available in the [openQA source code repository](https://github.com/os-autoinst/openQA), which can be cloned with the following command:
```
git clone https://github.com/os-autoinst/openQA.git
```
If you have GitHub setup properly with SSH, you might use this instead:
```
git clone git@github.com:os-autoinst/openQA.git
```
The charts themselves are stored in `container/helm`. You can change your working directory to that location:
```
cd openQA/container/helm
```
### Chart Structure
The `charts` folder contains several different charts:
- `openqa` -- Parent chart.
- `webui` -- Web interface and API for openQA.
- `worker` -- openQA worker that performs the actual test execution.
We will be working with the parent chart `openqa`. Since it references the other charts, they will be handled automatically by the parent.
## Installing openQA
We then have to update dependencies and install openQA:
```
$ helm dependency update charts/openqa/
$ helm install openqa charts/openqa/
```
We can also list installed releases to confirm that openQA was deployed:
```
$ helm list
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
openqa default 1 2025-06-19 12:39:08.150021755 +0200 CEST deployed openqa-0.1.0
```
The minikube dashboard also lists all running pods:
![Minikube Dashboard, Pods](minikube-dashboard-pods.png)
### Accessing openQA WebUI
Since openQA is confined to its own network within minikube, it is not directly accessible from the host system. Below are the services running inside minikube:
```
$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
db ClusterIP 10.103.192.20 <none> 5432/TCP 6m34s
db-hl ClusterIP None <none> 5432/TCP 6m34s
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 174m
openqa-webui ClusterIP 10.104.24.71 <none> 9526/TCP,9527/TCP,9528/TCP,9529/TCP 6m34s
```
First, we need to create a proxy within the minikube network:
```
minikube tunnel
```
The openQA WebUI is running at `10.104.24.71` on port `9526` and is now accessible directly from the browser.
![openQA Dashboard](openqa-dashboard.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -23,6 +23,8 @@ The Act Runner reads the private key from a secret and uses it for SSH authentic
Zola is also installed by pulling a built release tar and extracting the binary. I have tried using the official Zola container, but it just would not work properly. Zola is also installed by pulling a built release tar and extracting the binary. I have tried using the official Zola container, but it just would not work properly.
Here is an example from this very website [deployment action](https://git.0x45.cz/em/em.0x45.cz/src/branch/master/.gitea/workflows/deploy.yaml):
```yaml ```yaml
name: Build name: Build
@ -32,9 +34,9 @@ on:
- master - master
env: env:
ZOLA_VERSION: "0.18.0" ZOLA_VERSION: ${{ vars.ZOLA_VERSION }}
HOST: ${{ secrets.SSH_HOSTNAME }} HOST: ${{ vars.HOST }}
HOST_DIR: ${{ secrets.SSH_TARGET_DIR }} HOST_DIR: ${{ vars.HOST_DIR }}
SSH_USERNAME: ${{ secrets.SSH_USERNAME }} SSH_USERNAME: ${{ secrets.SSH_USERNAME }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
@ -64,6 +66,8 @@ jobs:
rsync -r --delete-after public/* "${SSH_USERNAME}@${HOST}:${HOST_DIR}" rsync -r --delete-after public/* "${SSH_USERNAME}@${HOST}:${HOST_DIR}"
``` ```
The example cinfiguration uses both secrets and vars from within Gitea. These have to be set trough the web UI under *Settings > Actions*.
## Webserver configuration ## Webserver configuration
The server needs a new user with write access to the website root directory. I still call it drone for the sake of not having to redo my server configuration. The server needs a new user with write access to the website root directory. I still call it drone for the sake of not having to redo my server configuration.
@ -88,7 +92,13 @@ Public key has to be added to `~/.ssh/authorized_keys` of the "drone" user.
| | | | | |
|-------------------|------------------------| |-------------------|------------------------|
| `SSH_HOSTNAME` | Server hostname |
| `SSH_TARGET_DIR` | Website root directory |
| `SSH_USERNAME` | In our case "drone" | | `SSH_USERNAME` | In our case "drone" |
| `SSH_PRIVATE_KEY` | Plaintext private key | | `SSH_PRIVATE_KEY` | Plaintext private key |
## Vars
| | |
|-------------------|------------------------|
| `ZOLA_VERSION` | Version of Zola |
| `HOST` | Server hostname |
| `TARGET_DIR` | Website root directory |

View File

@ -3,24 +3,24 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url("/fonts/spectral-regular.woff2") format("woff2"), src: url("/fonts/spectral-regular.woff2") format("woff2"),
url("/fonts/spectral-regular.woff") format("woff"), url("/fonts/spectral-regular.woff") format("woff"),
url("/fonts/spectral-regular.ttf") format("truetype"); url("/fonts/spectral-regular.ttf") format("truetype");
} }
@font-face { @font-face {
font-family: spectral; font-family: spectral;
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: url("/fonts/spectral-bold.woff2") format("woff2"), src: url("/fonts/spectral-bold.woff2") format("woff2"),
url("/fonts/spectral-bold.woff") format("woff"), url("/fonts/spectral-bold.woff") format("woff"),
url("/fonts/spectral-bold.ttf") format("truetype"); url("/fonts/spectral-bold.ttf") format("truetype");
} }
@font-face { @font-face {
font-family: spectral; font-family: spectral;
font-style: italic; font-style: italic;
font-weight: 400; font-weight: 400;
src: url("/fonts/spectral-italic.woff2") format("woff2"), src: url("/fonts/spectral-italic.woff2") format("woff2"),
url("/fonts/spectral-italic.woff") format("woff"), url("/fonts/spectral-italic.woff") format("woff"),
url("/fonts/spectral-italic.ttf") format("truetype"); url("/fonts/spectral-italic.ttf") format("truetype");
} }
::selection{ ::selection{
@ -28,6 +28,8 @@
color: #000; color: #000;
} }
html { scroll-behavior: smooth }
.wrap { .wrap {
max-width: 55rem; max-width: 55rem;
margin: 0 auto; margin: 0 auto;
@ -81,6 +83,9 @@ nav {
align-self: start; align-self: start;
top: 2em; top: 2em;
margin-bottom: 4rem; margin-bottom: 4rem;
max-height: calc(100vh - 2em);
box-sizing: border-box;
overflow-y: auto;
span.title { span.title {
display: block; display: block;
@ -94,7 +99,8 @@ nav {
li a { li a {
color: #000; color: #000;
display: block; display: block;
padding: 0 .5em; line-height: 1;
padding: .25em .5em;
&:hover { &:hover {
color: #fff; color: #fff;
@ -102,7 +108,35 @@ nav {
text-decoration: none; text-decoration: none;
} }
} }
li.mobile-only { display: none } }
ul.table-of-contents li {
a {
color: #e1140a;
position: relative;
&::before, &::after {
content: "";
background-color: #bbb;
position: absolute;
left: 0;
top: 0;
}
&::before {
height: 2px;
width: .5em;
}
&::after {
width: 2px;
height: .5em;
}
&:hover { color: #fff }
&:hover::before, &:hover::after { display: none }
}
li {
padding: 0 .5em;
&>a::before, &>a::after { display: none }
}
} }
} }
@ -189,7 +223,6 @@ main {
} }
input.search { input.search {
display: none; // Hide for non-js browsers
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
border: 1px solid #ccc; border: 1px solid #ccc;
@ -212,23 +245,36 @@ a {
&:hover { text-decoration: underline } &:hover { text-decoration: underline }
} }
.profile-picture {
float: right;
width: 15rem;
margin-left: 3em;
}
@media screen and (max-width: 50rem) { @media screen and (max-width: 50rem) {
.grid { grid-template-columns: auto } .grid { grid-template-columns: auto }
header { margin-bottom: 0 }
nav { nav {
position: initial; position: initial;
margin-bottom: 1rem;
ul { display: none } ul { display: none }
ul.main { ul.main {
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
border: 1px solid #43494c;
padding: .25rem 0;
} }
ul li.mobile-only { display: list-item }
span.title { display: none } span.title { display: none }
} }
header, nav { margin-bottom: 3rem } .profile-picture {
width: 10rem;
margin-left: 1em;
}
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
@ -242,14 +288,19 @@ a {
color: inherit; color: inherit;
} }
nav ul li a { nav ul li a, nav ul.table-of-contents li a {
color: inherit; color: inherit;
&:hover { &:hover {
color: #131516; color: #131516;
background-color: #cdcdcd; background-color: #cdcdcd;
} }
} }
nav ul.table-of-contents li a {
color: #ff6e67;
&::before, &::after {
background-color: #383838;
}
}
main { main {
.info, input.search { border-color: #383838 } .info, input.search { border-color: #383838 }

View File

@ -1,11 +1,23 @@
function filter_name(str) function filter_title(query, articles)
{ {
if (str.length == 0) { const search = query.trim().toLowerCase();
articles.forEach(article => article.style.display = "block");
} else { if (search.length === 0) {
articles.forEach(article => article.style.display = article.dataset.title.indexOf(str.toLowerCase()) === -1 ? "none" : "block"); articles.forEach(article => article.style.display = 'block');
return;
} }
articles.forEach(article => {
const title = article.dataset.title || '';
article.style.display = title.includes(search) ? 'block' : 'none';
});
} }
let articles = Array.from(document.querySelectorAll("article")); window.addEventListener('DOMContentLoaded', () => {
document.querySelector("#search").style.display = "inline-block"; document.querySelector('.search-wrapper').innerHTML =
'<input class="search" type="search" placeholder="Search">';
const search = document.querySelector("input[type='search']");
const articles = Array.from(document.querySelectorAll('article'));
search.addEventListener('input', () => filter_title(search.value, articles));
});

View File

@ -26,13 +26,13 @@
<span class="title">Navigation</span> <span class="title">Navigation</span>
<ul class="main"> <ul class="main">
{% for item in config.extra.nav %} {% for item in config.extra.nav %}
<li <li><a href="{{ get_url(path=item.path, trailing_slash=true) }}">{{ item.title }}</a></li>
{% if item.mobile_only %}
class="mobile-only"
{% endif %}
><a href="{{ get_url(path=item.path, trailing_slash=true) }}">{{ item.title }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% if page.toc and not page.extra.notoc %}
<span class="title">Table of Contents</span>
{{ macros::toc() }}
{% endif %}
<span class="title">Categories</span> <span class="title">Categories</span>
{{ macros::list_taxonomy(kind="categories") }} {{ macros::list_taxonomy(kind="categories") }}
</nav> </nav>

View File

@ -48,24 +48,22 @@
{% endmacro %} {% endmacro %}
{% macro toc() %} {% macro toc() %}
{% if page.toc and not page.extra.notoc %} <ul class="table-of-contents">
<ul> {% for h1 in page.toc %}
{% for h1 in page.toc %} <li>
<li> <a href="{{ h1.permalink | safe }}">{{ h1.title }}</a>
<a href="{{ h1.permalink | safe }}">{{ h1.title }}</a> {% if h1.children %}
{% if h1.children %} <ul>
<ul> {% for h2 in h1.children %}
{% for h2 in h1.children %} <li>
<li> <a href="{{ h2.permalink | safe }}">{{ h2.title }}</a>
<a href="{{ h2.permalink | safe }}">{{ h2.title }}</a> </li>
</li> {% endfor %}
{% endfor %} </ul>
</ul> {% endif %}
{% endif %} </li>
</li> {% endfor %}
{% endfor %} </ul>
</ul>
{% endif %}
{% endmacro %} {% endmacro %}
{% macro list_taxonomy(kind, page=false, prepend="") %} {% macro list_taxonomy(kind, page=false, prepend="") %}

View File

@ -6,7 +6,6 @@
{% block content %} {% block content %}
<h1>{{ page.title }}</h1> <h1>{{ page.title }}</h1>
{{ macros::toc() }}
{{ page.content | safe }} {{ page.content | safe }}
{{ macros::page_info(page=page) }} {{ macros::page_info(page=page) }}
{{ macros::page_updates(page=page) }} {{ macros::page_updates(page=page) }}

View File

@ -6,7 +6,7 @@
{% block content %} {% block content %}
<h1>{{ section.title }}</h1> <h1>{{ section.title }}</h1>
<input id="search" class="search" type="text" placeholder="Search titles" oninput="filter_name(this.value)"> <div class="search-wrapper"></div>
{{ macros::list_posts() }} {{ macros::list_posts() }}
{% endblock content %} {% endblock content %}