Compare commits
15 Commits
c19dbf3e44
...
master
Author | SHA1 | Date | |
---|---|---|---|
8c9da1d33f | |||
c311b67b04 | |||
93c77ebd48 | |||
ad3ba54726 | |||
3085297dbe | |||
9c15c4d4d4 | |||
e3dcd4c6b0 | |||
0f6cc9b1fc | |||
e7d373f886 | |||
b851a127c7 | |||
a2b355477f | |||
6b245f540d | |||
85f0e72676 | |||
ee77ff7c84 | |||
f0571e9e38 |
38
.gitea/workflows/deploy.yaml
Normal file
38
.gitea/workflows/deploy.yaml
Normal 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}"
|
@ -21,7 +21,6 @@ nav = [
|
||||
{title = "Index", path = "/"},
|
||||
{title = "About", path = "/about/"},
|
||||
{title = "Search", path = "/posts/"},
|
||||
{title = "Categories", path = "/categories/", mobile_only = true}
|
||||
]
|
||||
latest_posts_count = 3
|
||||
repository_url = "https://git.0x45.cz/em/em.0x45.cz"
|
||||
|
@ -4,13 +4,15 @@ title = "About"
|
||||
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 am working on.
|
||||
|
||||
It is generated to static HTML via [Zola](https://www.getzola.org/).
|
||||
|
||||
## Contact me
|
||||
|
||||
- [em@0x45.cz](mailto:em@0x45.cz)
|
||||
- [em@0x45.cz](mailto:em@0x45.cz)
|
||||
- [@irungentoo](https://t.me/irungentoo) on Telegram
|
||||
- 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/).
|
||||
|
||||
## 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
|
||||
|
||||
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.microlab.space/em](https://git.microlab.space/em)
|
BIN
content/about/profile-picture.jpg
Normal file
BIN
content/about/profile-picture.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
@ -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:
|
||||
|
||||

|
||||
|
||||
|
||||
### 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.
|
||||
|
||||

|
Binary file not shown.
After Width: | Height: | Size: 102 KiB |
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
@ -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.
|
||||
|
||||
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
|
||||
name: Build
|
||||
|
||||
@ -32,9 +34,9 @@ on:
|
||||
- master
|
||||
|
||||
env:
|
||||
ZOLA_VERSION: "0.18.0"
|
||||
HOST: ${{ secrets.SSH_HOSTNAME }}
|
||||
HOST_DIR: ${{ secrets.SSH_TARGET_DIR }}
|
||||
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 }}
|
||||
|
||||
@ -64,6 +66,8 @@ jobs:
|
||||
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
|
||||
|
||||
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_PRIVATE_KEY` | Plaintext private key |
|
||||
|
||||
## Vars
|
||||
|
||||
| | |
|
||||
|-------------------|------------------------|
|
||||
| `ZOLA_VERSION` | Version of Zola |
|
||||
| `HOST` | Server hostname |
|
||||
| `TARGET_DIR` | Website root directory |
|
||||
|
@ -28,6 +28,8 @@
|
||||
color: #000;
|
||||
}
|
||||
|
||||
html { scroll-behavior: smooth }
|
||||
|
||||
.wrap {
|
||||
max-width: 55rem;
|
||||
margin: 0 auto;
|
||||
@ -81,6 +83,9 @@ nav {
|
||||
align-self: start;
|
||||
top: 2em;
|
||||
margin-bottom: 4rem;
|
||||
max-height: calc(100vh - 2em);
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
|
||||
span.title {
|
||||
display: block;
|
||||
@ -94,7 +99,8 @@ nav {
|
||||
li a {
|
||||
color: #000;
|
||||
display: block;
|
||||
padding: 0 .5em;
|
||||
line-height: 1;
|
||||
padding: .25em .5em;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
@ -102,7 +108,35 @@ nav {
|
||||
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 {
|
||||
display: none; // Hide for non-js browsers
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
border: 1px solid #ccc;
|
||||
@ -212,23 +245,36 @@ a {
|
||||
&:hover { text-decoration: underline }
|
||||
}
|
||||
|
||||
.profile-picture {
|
||||
float: right;
|
||||
width: 15rem;
|
||||
margin-left: 3em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 50rem) {
|
||||
|
||||
.grid { grid-template-columns: auto }
|
||||
|
||||
header { margin-bottom: 0 }
|
||||
|
||||
nav {
|
||||
position: initial;
|
||||
margin-bottom: 1rem;
|
||||
ul { display: none }
|
||||
ul.main {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
border: 1px solid #43494c;
|
||||
padding: .25rem 0;
|
||||
}
|
||||
ul li.mobile-only { display: list-item }
|
||||
span.title { display: none }
|
||||
}
|
||||
|
||||
header, nav { margin-bottom: 3rem }
|
||||
.profile-picture {
|
||||
width: 10rem;
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@ -242,14 +288,19 @@ a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
nav ul li a {
|
||||
nav ul li a, nav ul.table-of-contents li a {
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
color: #131516;
|
||||
background-color: #cdcdcd;
|
||||
}
|
||||
}
|
||||
nav ul.table-of-contents li a {
|
||||
color: #ff6e67;
|
||||
&::before, &::after {
|
||||
background-color: #383838;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
.info, input.search { border-color: #383838 }
|
||||
|
@ -1,11 +1,23 @@
|
||||
function filter_name(str)
|
||||
function filter_title(query, articles)
|
||||
{
|
||||
if (str.length == 0) {
|
||||
articles.forEach(article => article.style.display = "block");
|
||||
} else {
|
||||
articles.forEach(article => article.style.display = article.dataset.title.indexOf(str.toLowerCase()) === -1 ? "none" : "block");
|
||||
const search = query.trim().toLowerCase();
|
||||
|
||||
if (search.length === 0) {
|
||||
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"));
|
||||
document.querySelector("#search").style.display = "inline-block";
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
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));
|
||||
});
|
||||
|
@ -26,13 +26,13 @@
|
||||
<span class="title">Navigation</span>
|
||||
<ul class="main">
|
||||
{% for item in config.extra.nav %}
|
||||
<li
|
||||
{% if item.mobile_only %}
|
||||
class="mobile-only"
|
||||
{% endif %}
|
||||
><a href="{{ get_url(path=item.path, trailing_slash=true) }}">{{ item.title }}</a></li>
|
||||
<li><a href="{{ get_url(path=item.path, trailing_slash=true) }}">{{ item.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if page.toc and not page.extra.notoc %}
|
||||
<span class="title">Table of Contents</span>
|
||||
{{ macros::toc() }}
|
||||
{% endif %}
|
||||
<span class="title">Categories</span>
|
||||
{{ macros::list_taxonomy(kind="categories") }}
|
||||
</nav>
|
||||
|
@ -48,8 +48,7 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% macro toc() %}
|
||||
{% if page.toc and not page.extra.notoc %}
|
||||
<ul>
|
||||
<ul class="table-of-contents">
|
||||
{% for h1 in page.toc %}
|
||||
<li>
|
||||
<a href="{{ h1.permalink | safe }}">{{ h1.title }}</a>
|
||||
@ -65,7 +64,6 @@
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro list_taxonomy(kind, page=false, prepend="") %}
|
||||
|
@ -6,7 +6,6 @@
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ page.title }}</h1>
|
||||
{{ macros::toc() }}
|
||||
{{ page.content | safe }}
|
||||
{{ macros::page_info(page=page) }}
|
||||
{{ macros::page_updates(page=page) }}
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
{% block content %}
|
||||
<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() }}
|
||||
{% endblock content %}
|
||||
|
||||
|
Reference in New Issue
Block a user