Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fe65028
Development: Add Kubernetes-native LocalCI execution
krusche Aug 1, 2026
8ac1f3e
implement small improvements
krusche Aug 1, 2026
cab3f2a
Merge branch 'develop' into feature/kubernetes-localci
krusche Aug 1, 2026
aec98e5
Merge branch 'develop' into feature/kubernetes-localci
krusche Aug 2, 2026
9a05f8c
Merge remote-tracking branch 'origin/develop' into feature/kubernetes…
krusche Aug 3, 2026
c65e68f
Infrastructure: Address Kubernetes LocalCI review feedback
krusche Aug 3, 2026
689af11
Infrastructure: Publish the requeued build job atomically with its state
krusche Aug 3, 2026
a93fab7
Infrastructure: Cover the Hazelcast wire compatibility of the agent d…
krusche Aug 3, 2026
6f5b9b8
Infrastructure: Cap requeues from the pause grace period too
krusche Aug 3, 2026
1191314
Merge remote-tracking branch 'origin/develop' into feature/kubernetes…
krusche Aug 3, 2026
bf12c09
Merge remote-tracking branch 'origin/develop' into HEAD
krusche Aug 10, 2026
542f889
Merge remote-tracking branch 'origin/develop' into HEAD
krusche Aug 11, 2026
b439915
Infrastructure: Address the review findings on the Kubernetes runner
krusche Aug 11, 2026
bd3fc74
Merge develop into feature/kubernetes-localci
krusche Aug 12, 2026
7598b44
Merge remote-tracking branch 'origin/develop' into remerge-13375
krusche Aug 12, 2026
6670618
Merge remote-tracking branch 'origin/develop' into feature/kubernetes…
krusche Aug 18, 2026
cbe9e2b
Merge remote-tracking branch 'origin/develop' into feature/kubernetes…
krusche Aug 19, 2026
97d8a6e
Infrastructure: Collect Kubernetes build results from the build direc…
krusche Aug 19, 2026
a7b6044
Infrastructure: Guard the Kubernetes teardown by kubectl context
krusche Aug 23, 2026
d383197
Merge branch 'develop' into feature/kubernetes-localci
krusche Aug 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ data-exports/
/src/test/playwright/ssh-keys/known_hosts
/src/test/playwright/.auth/

# Local Docker Desktop Kubernetes acceptance state
/.localci-kubernetes/

#################################
# Files generated by prebuild.mjs
#################################
Expand Down
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,13 @@ dependencies {
// Note: we explicitly use docker-java-transport-zerodep, because docker-java-transport-httpclient5 uses an outdated http5 version which is not compatible with Spring Boot >= 3.4.0
implementation "com.github.docker-java:docker-java-transport-zerodep:${docker_java_version}"

// LocalCI Kubernetes build runner. Artemis already uses Vert.x in other modules, so use the
// dedicated OkHttp transport to keep the Kubernetes client's transport isolated.
implementation("io.fabric8:kubernetes-client:${kubernetes_client_version}") {
exclude group: "io.fabric8", module: "kubernetes-httpclient-vertx"
}
implementation "io.fabric8:kubernetes-httpclient-okhttp:${kubernetes_client_version}"

// use newest version of commons-compress to avoid security issues through outdated dependencies
implementation "org.apache.commons:commons-compress:1.28.0"

Expand Down
12 changes: 12 additions & 0 deletions docker/localci-kubernetes-helper/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM alpine:3.22

# ip6tables is a separate package on Alpine; without it the network isolation init container
# cannot apply any IPv6 rules and builds requested with network mode "none" stay reachable over IPv6.
RUN apk add --no-cache bash coreutils ip6tables iptables tar

# No USER directive on purpose: the Kubernetes Job spec sets the effective user per container.
# The helper and the isolation container both run as UID 0 because they share the workspace volume
# with a builder container that usually runs as root, and the isolation container needs NET_ADMIN.
WORKDIR /var/tmp

CMD ["sh", "-c", "while true; do sleep 3600; done"]

Check warning on line 12 in docker/localci-kubernetes-helper/Dockerfile

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

docker/localci-kubernetes-helper/Dockerfile#L12

By not specifying a USER, a program in the container may run as 'root'. This is a security hazard.
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ slf4j_version=2.0.18
sentry_version=8.47.0
liquibase_version=5.0.3
docker_java_version=3.7.1
kubernetes_client_version=7.7.0
# logback versions managed by Spring Boot BOM
byte_buddy_version=1.18.10
mysql_connector_version=9.7.0
Expand Down
11 changes: 11 additions & 0 deletions helm/artemis/.helmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Patterns to ignore when building packages.
*.tmpl
.DS_Store
.git/
.gitignore
*.swp
*.bak
*.orig
*~
.vscode/
.idea/
82 changes: 82 additions & 0 deletions helm/artemis/CLUSTER-SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Cluster setup and validation

## Docker Desktop

The local acceptance target is Docker Desktop's managed Kubernetes cluster, not a separately installed `kind` binary.

1. Open Docker Desktop settings.
2. Enable the containerd image store.
3. Under Kubernetes, choose the `kind` provisioner and configure three nodes.
4. Apply/recreate the cluster and wait until it is running.
5. Select the `docker-desktop` context:

```bash
kubectl config use-context docker-desktop
kubectl get nodes
```

The expected topology is one control-plane and two schedulable workers. The helper script refuses a one-node or `kubeadm` Docker Desktop cluster because that
would not exercise multi-node placement.

Run all local steps:

```bash
./run-localci-kubernetes.sh all
```

Individual commands are available:

```bash
./run-localci-kubernetes.sh build
./run-localci-kubernetes.sh up
./run-localci-kubernetes.sh status
./run-localci-kubernetes.sh test
./run-localci-kubernetes.sh logs
./run-localci-kubernetes.sh down
```

`up --skip-build` reuses existing images. `test --filter <playwright-filter>` forwards an optional Playwright filter. `all --keep` leaves the installation and
port-forward running after validation. `down` removes the release, both namespaces, and the Artemis-specific worker labels while retaining the local images.

## Scheduling labels

The local script labels one worker for both core pods and both workers for controllers/workloads:

```text
artemis.cit.tum.de/core=true
artemis.cit.tum.de/build-worker=true
```

The core pin is only for Docker Desktop's `ReadWriteOnce` volume. Do not copy it into a real cluster; provide `ReadWriteMany` storage instead.

## Verification

Useful checks after installation:

```bash
kubectl -n artemis get pods -o wide
kubectl -n artemis-builds get jobs,pods -w
kubectl auth can-i create jobs.batch \
--as system:serviceaccount:artemis:artemis-localci-controller \
-n artemis-builds
kubectl auth can-i list secrets \
--as system:serviceaccount:artemis-builds:artemis-localci-workload \
-n artemis-builds
```
Comment thread
krusche marked this conversation as resolved.

The first authorization check should return `yes`; the workload check should return `no`.

The chart exposes `artemis-http` as a ClusterIP. For local access:

```bash
kubectl -n artemis port-forward service/artemis-http 8080:8080
```

## Real clusters

- Use an RWX storage class for `sharedStorage`.
- Use a managed PostgreSQL/broker if availability is required.
- Configure a public `artemis.config.serverUrl` that is also resolvable from build-agent pods.
- Publish both the Artemis image and trusted helper image to a registry and configure pull secrets.
- Keep the controller and workload service accounts separate.
- Add network policy, admission controls, quotas, runtime isolation, TLS, and production secret management according to local requirements.
30 changes: 30 additions & 0 deletions helm/artemis/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
apiVersion: v2
name: artemis
description: >-
Multi-node Artemis deployment with LocalCI core nodes and Kubernetes build agents.
type: application

# Chart version. Bump on every change to the chart or its templates (SemVer).
version: 0.2.0

# The Artemis application version this chart defaults to. Overridable via `image.tag`.
# The application image must contain the LocalCI Kubernetes runner.
appVersion: "9.9"

home: https://docs.artemis.cit.tum.de
sources:
- https://github.com/ls1intum/Artemis

maintainers:
- name: Artemis Team
url: https://github.com/ls1intum/Artemis

icon: https://raw.githubusercontent.com/ls1intum/Artemis/develop/src/main/resources/public/images/logo.png

keywords:
- artemis
- e-learning
- postgres
- localci
- kubernetes
- gateway-api
76 changes: 76 additions & 0 deletions helm/artemis/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Artemis multi-node LocalCI chart

This MVP chart deploys a complete Artemis LocalCI installation in Kubernetes:

- one Artemis leader core with the `scheduling` profile;
- zero or more member cores without `scheduling`;
- PostgreSQL, JHipster Registry/Eureka, and ActiveMQ STOMP relay;
- standalone LocalCI build-agent controllers; and
- Kubernetes Jobs created directly for individual builds.

Docker execution remains the default outside the `k8s` profile. The controllers in this chart use
`prod,buildagent,k8s` and do not need a Docker socket.

## Execution flow

1. Core nodes place jobs in the existing distributed priority queue and calculate estimated start/completion times.
2. A controller claims work according to its configured concurrency.
3. The controller clones LocalVC repositories with the existing build-agent account.
4. The controller creates a Job in `buildAgents.namespace` with the exercise image and a trusted helper sidecar.
5. Repositories and the script are transferred through the Kubernetes exec API into an `emptyDir` workspace.
6. Build logs are streamed into the existing LocalCI log map. The helper returns the same tar result format used by Docker execution.
7. Existing XML/SARIF parsing, result queueing, cancellation, pause/resume, retry counts, statistics, and time estimates continue unchanged.

The workload service account has no API token and no permissions. The controller receives only the namespaced Job/Pod/log/exec permissions in
`templates/build-agents/serviceaccounts-rbac.yaml`.

## Storage and multi-core requirements

Every core must mount the same `/opt/artemis/data` filesystem. It contains LocalVC repositories, uploads, exports, and build logs. A real multi-node
cluster therefore needs `ReadWriteMany` storage. Docker Desktop has only local `ReadWriteOnce` storage, so
`values-docker-desktop.yaml` pins both core pods to one worker as an explicit local-only compromise.

Core nodes discover one another through Eureka and use ActiveMQ for cross-core WebSocket delivery. Exactly one core has `scheduling`.

## Install

Create a values file with real secrets and a resolvable server URL, then run:

```bash
helm upgrade --install artemis ./helm/artemis \
--namespace artemis --create-namespace \
--values my-values.yaml \
--wait --timeout 20m
```

Required secret values are:

- `artemis.config.admin.password`
- `artemis.config.jwtBase64Secret`
- `artemis.config.versionControl.buildAgentGitPassword`
- `postgresql.auth.password`
- `registry.password`
- `broker.auth.password`

Set `gateway.enabled=false` when using only `kubectl port-forward`. For a plain-HTTP local port-forward, also set `artemis.config.secureCookies=false`; the Docker Desktop values file already does this. Otherwise install the Gateway API CRDs/controller and configure the gateway values.

## Docker Desktop acceptance cluster

Use Docker Desktop 4.51 or newer, switch its managed Kubernetes provisioner to `kind`, select three nodes, and use the containerd image store. Then run:

```bash
./run-localci-kubernetes.sh all
```

The script validates the context and topology, labels workers, builds/imports the two local images, installs this chart with
`values-docker-desktop.yaml`, and verifies that both cores register, the build-agent controllers occupy distinct workers, and native build Jobs run on both workers. See
`CLUSTER-SETUP.md` for commands and diagnostics.

## Supported exercise flags

The MVP supports environment variables (`KEY=value`), CPU, memory, and `network=none`. Named Docker networks and custom memory swap are rejected with an
explicit build error. `network=none` is implemented by a short trusted init container with `NET_ADMIN`; the exercise and helper containers do not receive
that capability.

This MVP does not include runtime sandboxes such as gVisor/Kata, production secret management, autoscaling, highly available data services, object storage,
or named network profiles.
49 changes: 49 additions & 0 deletions helm/artemis/cluster-setup/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Cluster-setup manifests

Ready-to-apply manifests for the cluster-level prerequisites described in
[`../CLUSTER-SETUP.md`](../CLUSTER-SETUP.md). **Edit the `FIXME:` placeholders** (IP ranges, email, namespace, release
name) before applying.

```
cluster-setup/
├── gatewayclass.yaml # plain Envoy Gateway GatewayClass
├── clusterissuer-letsencrypt.yaml # optional: cert-manager Let's Encrypt issuer
└── metallb-dualstack/ # dual-stack Envoy LB pinned to a MetalLB pool
├── envoyproxy.yaml # references an EXISTING MetalLB pool by name
└── gatewayclass.yaml # GatewayClass wired to the EnvoyProxy
```

> These manifests reference an **existing** MetalLB `IPAddressPool` (by name, in `envoyproxy.yaml`); they do not create
> one. The pool must contain both an IPv4 and an IPv6 range for dual-stack to work.

## What to apply

CRDs and controllers are installed with `helm`/`kubectl` per `../CLUSTER-SETUP.md` (Gateway API experimental CRDs,
Envoy Gateway, cert-manager, an RWX StorageClass). Once those are in place, apply the manifests here.

Pick **one** GatewayClass:

**A. Plain (no MetalLB customization):**

```bash
kubectl apply -f cluster-setup/gatewayclass.yaml
```

**B. MetalLB pool + dual-stack** (applies the EnvoyProxy and a GatewayClass that references it; set the pool name in
`envoyproxy.yaml` first):

```bash
kubectl apply -f cluster-setup/metallb-dualstack/
```

> Requires Envoy Gateway's own CRDs (the `EnvoyProxy` kind). If you installed the controller with `--skip-crds`, apply
> them first - see `../CLUSTER-SETUP.md` section 2 - otherwise this fails with
> `no matches for kind "EnvoyProxy"`.

Optional TLS (after cert-manager is installed):

```bash
kubectl apply -f cluster-setup/clusterissuer-letsencrypt.yaml
```

All variants create a GatewayClass named `envoy`, so set `gateway.className=envoy` in your chart values.
21 changes: 21 additions & 0 deletions helm/artemis/cluster-setup/clusterissuer-letsencrypt.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Optional: cert-manager ClusterIssuer that solves ACME HTTP-01 challenges
# through the Gateway API, so the chart's HTTPS listener gets an automatic
# Let's Encrypt certificate. Requires cert-manager to be installed.
# Set gateway.tls.certManagerClusterIssuer=letsencrypt-prod in your chart values.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com # FIXME: your contact email
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
gatewayHTTPRoute:
parentRefs:
- name: <gateway-name> # FIXME: the rendered Gateway resource name
namespace: <ns> # FIXME: the release namespace
kind: Gateway
9 changes: 9 additions & 0 deletions helm/artemis/cluster-setup/gatewayclass.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Plain Envoy Gateway GatewayClass (no MetalLB / dual-stack customization).
# Apply this OR the metallb-dualstack/ variant, not both (same name "envoy").
# Set gateway.className=envoy in your chart values.
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: envoy
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
29 changes: 29 additions & 0 deletions helm/artemis/cluster-setup/metallb-dualstack/envoyproxy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Customizes the LoadBalancer service Envoy Gateway generates: pins it to a
# specific MetalLB pool and requests dual-stack (IPv4 + IPv6). Referenced by the
# GatewayClass in this folder via parametersRef.
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
name: artemis-envoy-proxy
namespace: envoy-gateway-system # must live in the Envoy Gateway namespace
spec:
provider:
type: Kubernetes
kubernetes:
envoyService:
annotations:
# Pin the LB address to one of your EXISTING MetalLB pools (must contain
# both an IPv4 and an IPv6 range for dual-stack). This chart does NOT
# create the pool.
metallb.universe.tf/address-pool: lb2 # FIXME
# Optional: request specific IPs from that pool (comma-separated, one per family).
# metallb.universe.tf/loadBalancerIPs: 192.0.2.240,2001:db8:42::1
patch:
# No dedicated field for ipFamilies, so patch the generated Service.
type: StrategicMerge
value:
spec:
ipFamilyPolicy: RequireDualStack # use PreferDualStack to fall back gracefully
ipFamilies:
- IPv4
- IPv6
14 changes: 14 additions & 0 deletions helm/artemis/cluster-setup/metallb-dualstack/gatewayclass.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Envoy Gateway GatewayClass that references the EnvoyProxy in this folder,
# applying the MetalLB pool + dual-stack customization to every Gateway of this
# class. Set gateway.className=envoy in your chart values.
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: envoy
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
parametersRef:
group: gateway.envoyproxy.io
kind: EnvoyProxy
name: artemis-envoy-proxy
namespace: envoy-gateway-system
Loading
Loading