Compare commits

...

10 Commits

Author SHA1 Message Date
timotheereausanofi
3e5dfaa1cb docs(gitops): single Argo CD README, remove redundant docs
Made-with: Cursor
2026-03-20 12:06:20 +01:00
timotheereausanofi
1dc04c9fc5 fix(grafana): disable init-chown-data for Pod Security / k3s
Made-with: Cursor
2026-03-20 11:34:52 +01:00
timotheereausanofi
4f66f7f7ed feat(observability): OneLab-only Promtail, provisioned OneLab logs dashboard
- Promtail: keep kubernetes-pods in namespace onelab; tag host file logs (host-logs)
- Grafana: enable dashboard sidecar; ConfigMap onelab-logs.json
- Dashboard: stats (total/error/warn heuristics), logs panel, component + regex filters

Made-with: Cursor
2026-03-20 11:28:47 +01:00
timotheereausanofi
3802418582 fix(argocd): multisource patch doc, Ingress grafana-onelab
- Root cause: live Application kept spec.source; Argo ignored observability chart
- Add jsonpatch-multisource.json + argocd/README.md migration steps
- Grafana: disable subchart ingress; add templates/ingress-grafana-onelab.yaml

Made-with: Cursor
2026-03-20 11:13:55 +01:00
timotheereausanofi
b91c35c410 gitops: observability stack (Loki/Promtail/Grafana), Grafana Ingress, Argo multi-source
- Add gitops/observability umbrella chart with vendored Helm deps
- Grafana Ingress: Traefik, letsencrypt-prod, grafana.k8s.selair.it + root_url
- Argo Application: spec.sources (onelab + onelab-obs)
- OneLab: configuration secret override, compliance/LDAP values, logs.path /logs
- Docs: OBSERVABILITY, BOOTSTRAP, README, instance-overrides example

Made-with: Cursor
2026-03-20 11:10:06 +01:00
timotheereausanofi
9cb1b10d6c ingress: TLS via cert-manager (letsencrypt-prod) for onelab.k8s.selair.it
Made-with: Cursor
2026-03-20 10:29:50 +01:00
timotheereausanofi
279829cfee feat(ingress): Traefik ingress to revproxy for web UI; ClusterIP revproxy in k3s example
Made-with: Cursor
2026-03-20 10:27:51 +01:00
timotheereausanofi
e0e294a944 fix(statefulset): roll pods when docker registry auth changes; doc stale pull secret recovery
Made-with: Cursor
2026-03-20 10:25:47 +01:00
timotheereausanofi
e2d50d8d16 Use Swarm default registry creds (manage-images) and configurations.yml placeholders
Made-with: Cursor
2026-03-20 10:22:05 +01:00
timotheereausanofi
4ef10ffc20 docs: bootstrap Argo Git auth and registry pull secret
Made-with: Cursor
2026-03-20 10:16:07 +01:00
24 changed files with 918 additions and 62 deletions

View File

@@ -1,52 +1,217 @@
# OneLab GitOps (k3s + Argo CD)
# OneLab GitOps (Argo CD)
This directory holds the **Helm chart** that replaces `docker stack deploy` from the legacy Swarm installer (`app/docker-compose.yml`).
This directory is the **declarative source** for OneLab on Kubernetes. Argo CD applies two **Helm-based sources** from Git (Argo invokes Helm internally; you do not run a separate Helm install workflow).
Legacy Swarm install lives under [`app/`](../app/) (`docker-compose.yml`); this tree replaces `docker stack deploy` for k3s/Kubernetes.
## Layout
| Path | Purpose |
|------|---------|
| `charts/onelab` | Helm chart (StatefulSets, Deployments, Services, ConfigMaps, Secrets) |
| `values/*.yaml` | Environment-specific overrides (non-secret defaults; use sealed/external secrets for prod) |
| `argocd/application.yaml` | Example `Application` — set `repoURL` / `targetRevision` to your remote |
| [`charts/onelab`](charts/onelab) | OneLab chart (StatefulSets, Deployments, Services, ConfigMaps, Secrets)**Argo source 1** |
| [`values/`](values/) | Environment values (e.g. [`values/k3s-example.yaml`](values/k3s-example.yaml)); reference from `helm.valueFiles` |
| [`observability/`](observability/) | Loki / Promtail / Grafana umbrella chart — **Argo source 2** (`releaseName: onelab-obs`) |
| [`argocd/application.yaml`](argocd/application.yaml) | `Application` manifest (`spec.sources`, namespace `onelab`) |
| [`argocd/jsonpatch-multisource.json`](argocd/jsonpatch-multisource.json) | One-time JSON patch if the live `Application` stuck on `spec.source` |
## Prerequisites
1. **k3s** (or any Kubernetes) with default storage class for Postgres/Rabbit PVCs (e.g. `local-path`).
2. **Image pull access** to `hub.andrewalliance.com`create a docker-registry secret and reference it in `imagePullSecrets`:
```bash
kubectl create namespace onelab
kubectl create secret docker-registry hub-andrewalliance -n onelab \
--docker-server=hub.andrewalliance.com --docker-username=... --docker-password=...
```
3. **RabbitMQ TLS secret** (name `onelab-rabbit-tls` by default) — see `values/k3s-example.yaml` comments, or set `rabbitmq.tls.embed: true` with PEM strings in a **private** values file.
4. **Host paths** (default): ensure `/opt/onelab/data` and `/opt/onelab/logs` exist on nodes that run workloads using `persistence.mode: hostPath`, or switch to RWX storage for multi-node.
1. **Kubernetes** (e.g. k3s) with a default **StorageClass** for Postgres/Rabbit PVCs (e.g. `local-path`).
2. **Image pull** to `hub.andrewalliance.com`registry Secret + `imagePullSecrets` (see [`values/k3s-example.yaml`](values/k3s-example.yaml) and [Private registry credentials](#private-registry-credentials)).
3. **RabbitMQ TLS** Secret `onelab-rabbit-tls` (or `rabbitmq.tls.embed` in a private values file) — [RabbitMQ TLS](#rabbitmq-tls).
4. **Host paths** when using `persistence.mode: hostPath`: `/opt/onelab/data` and `/opt/onelab/logs` on nodes that run those pods, or use RWX storage for multi-node.
## Helm (without Argo CD)
## Bootstrap (registry, Argo repo, TLS)
### Private registry credentials
By default, `gitops/values/k3s-example.yaml` matches the Swarm installer (`app/playbooks/tasks/manage-images.yml`): user **`public`**, password **`Andrew01..Release`**, and the chart creates Secret **`hub-andrewalliance`** when `registry.createPullSecret: true`.
To use other credentials, override `registry.username` / `registry.password` or create the secret manually:
```bash
cd gitops/charts/onelab
helm upgrade --install onelab . -n onelab --create-namespace \
-f ../../values/k3s-example.yaml
kubectl create secret docker-registry hub-andrewalliance -n onelab \
--docker-server=hub.andrewalliance.com \
--docker-username='YOUR_USER' \
--docker-password='YOUR_PASSWORD'
```
## Argo CD
…and set `registry.createPullSecret: false` plus `imagePullSecrets: [{ name: hub-andrewalliance }]`.
1. Push this repository to a Git remote Argo CD can read.
2. Edit `argocd/application.yaml`: `repoURL`, `targetRevision`, and values file as needed.
3. `kubectl apply -f gitops/argocd/application.yaml` (from a machine with a working kubeconfig).
#### StatefulSet pods still get `401 Unauthorized` / `ImagePullBackOff` after enabling registry auth
Sync waves order Postgres → Redis/Rabbit/config → application pods.
If `db-0` / `rabbitmq-0` were created **before** `imagePullSecrets` existed, their **Pod** spec can still use anonymous pulls until they are recreated:
```bash
kubectl delete pod -n onelab db-0 rabbitmq-0
```
The chart adds a pod-template checksum so after you change registry settings in Git and **Argo syncs**, workloads normally roll; a one-time delete is enough if pods were created before pull secrets existed.
### Argo CD private Git repository
If the Application shows `authentication required: Unauthorized`, register the repo in Argo CD (CLI or UI):
```bash
# Example; use a deploy token or PAT with repo read access
argocd repo add https://git.luneski.fr/luneski/onelab-k8s.git \
--username git \
--password YOUR_TOKEN
```
Then apply the Application:
```bash
kubectl apply -f gitops/argocd/application.yaml
```
**Single controller:** Use **only** this Argo CD `Application` for `onelab` / `onelab-obs`. Do not manage the same namespace with a separate **Helm CLI** release.
### RabbitMQ TLS
Secret `onelab-rabbit-tls` must exist before RabbitMQ starts (created once from `app/rabbit/ssl/` or your own PEMs).
### Argo CD version and observability stack
[`argocd/application.yaml`](argocd/application.yaml) uses **`spec.sources`** (two Helm charts in one Application). Use **Argo CD 2.6 or newer**.
If the `onelab` Application was created earlier with **`spec.source` only**, Argo will **not** show the observability resources until you remove `source` and set `sources` — see [Migrating `spec.source` → `spec.sources`](#migrating-specsource--specsources) below.
The second source installs Loki/Promtail/Grafana from [`observability/`](observability/) (`releaseName: onelab-obs`). Set a strong **`grafana.adminPassword`** in [`observability/values.yaml`](observability/values.yaml) before production — details in [Observability](#observability-loki--promtail--grafana).
## Deploy with Argo CD
1. Push this repo to a Git remote Argo CD can read.
2. Register the repo in Argo CD (CLI or UI) if it is private — [Argo CD private Git repository](#argo-cd-private-git-repository).
3. Edit [`argocd/application.yaml`](argocd/application.yaml): `repoURL`, `targetRevision`, and per-source `helm.valueFiles` if needed.
4. Apply the Application:
```bash
kubectl apply -f gitops/argocd/application.yaml
```
**Requirements:** Argo CD **2.6+** (`spec.sources`).
Each entry under `spec.sources` has its own `helm.releaseName` and `helm.valueFiles` (paths are **relative to that sources `path`**):
- Source `gitops/charts/onelab` → e.g. `../../values/k3s-example.yaml`
- Source `gitops/observability` → e.g. `values.yaml`
Both targets deploy into namespace **`onelab`**. Sync waves order: Postgres → Redis/Rabbit/config → application workloads.
### Migrating `spec.source` → `spec.sources`
If the `onelab` `Application` was created earlier with **`spec.source` only**, a plain `kubectl apply` of the new file may **not** remove `spec.source`, and Argo will never reconcile the observability chart.
Check:
```bash
kubectl get application onelab -n argocd -o jsonpath='{.spec.source}{"\n"}{.spec.sources}{"\n"}'
```
If `source` is set and `sources` is empty, patch once (adjust `repoURL` in the patch file if needed):
```bash
kubectl patch application onelab -n argocd --type json --patch-file gitops/argocd/jsonpatch-multisource.json
```
Then sync in Argo (or wait for auto-sync).
### Single controller
Manage these workloads **only** through this Argo CD `Application`. Do not drive the same resources with a parallel **Helm CLI** release.
### Logs / Grafana
See [Observability (Loki / Promtail / Grafana)](#observability-loki--promtail--grafana) — set a strong `grafana.adminPassword` in [`observability/values.yaml`](observability/values.yaml) before production.
## Observability (Loki / Promtail / Grafana)
The umbrella chart under [`observability/`](observability/) deploys:
- **Loki** — log storage (SingleBinary, filesystem PVC, 7-day retention by default).
- **Promtail** — DaemonSet: Kubernetes pod logs (`/var/log/pods`) plus **OneLab file logs** from the same host path the app chart uses (`/opt/onelab/logs` by default).
- **Grafana** — explore logs; datasource points at this releases Loki gateway.
It is synced by the **same** Argo CD Application as the OneLab chart ([`argocd/application.yaml`](argocd/application.yaml)): second `sources` entry, Argo **`helm.releaseName`** **`onelab-obs`** (so services are like `onelab-obs-loki-gateway`).
### First-time setup
1. **Change the Grafana admin password** in [`observability/values.yaml`](observability/values.yaml) (`grafana.adminPassword`) or switch to `admin.existingSecret` per the upstream Grafana chart.
2. **Align host paths** — if you change `persistence.hostPath.logs` for OneLab, update `promtail.extraVolumes` / `extraVolumeMounts` in the same `values.yaml` so Promtail still reads the shared log directory.
3. **Multi-node** — with `hostPath` logs, each node only sees its own files; Promtail runs on every node, so you still get coverage when pods move.
### OneLab-only ingestion
Promtail adds **`extraRelabelConfigs`** so the **kubernetes-pods** job **keeps only** pods in namespace **`onelab`**. Other namespaces no longer reach Loki (Explore only sees OneLab). Host file logs under `/opt/onelab/logs` are tagged with **`namespace: onelab`** and **`component: host-logs`** so they appear in the same queries.
Existing Loki data from before this change may still show non-`onelab` streams until **retention** drops them; for a clean index you would need to wipe the Loki PVC (destructive).
### Dashboard: **OneLab logs**
Grafanas **dashboard sidecar** loads ConfigMap **`…-dashboard-onelab-logs`** (JSON: `observability/dashboards/onelab-logs.json`). Open **Dashboards → OneLab logs** (`uid` `onelab-logs`):
- **Component** — multi-select from `label_values({namespace="onelab"}, component)` (includes **`host-logs`** for file logs).
- **Line filter** — regex applied to log line content (`.*` = all).
- Stat panels: total lines, heuristic **error** / **warning** counts (tuned for typical text logs, not strict JSON parsing).
#### Grafana pod: `init-chown-data` CrashLoopBackOff
The upstream chart runs an init container as **root** to `chown` `/var/lib/grafana`. Clusters with **Pod Security Admission** (often on k3s) commonly block that. This repo sets **`grafana.initChownData.enabled: false`**; the Grafana pod keeps **`fsGroup: 472`** so the PVC is usually group-writable. If Grafana still cannot write to disk, delete the Grafana PVC once after the change or relax PSA for namespace `onelab`.
### Access Grafana
An **Ingress** named **`grafana-onelab`** is created by the umbrella chart (`observability/templates/ingress-grafana-onelab.yaml`), Traefik + cert-manager, matching the OneLab web UI pattern in `gitops/values/k3s-example.yaml`:
- Host: **`grafana.k8s.selair.it`** — edit `grafanaOnelabIngress` and `grafana.ini.server` in `gitops/observability/values.yaml` together.
- TLS Secret: **`grafana-tls-k8s-selair`** (cert-manager with `letsencrypt-prod`).
Point DNS at your ingress, sync the app, then open `https://<grafana-host>/` (user `admin` until you change values).
For debugging without DNS:
```bash
kubectl -n onelab port-forward svc/onelab-obs-grafana 3000:80
```
### Maintainers: vendored chart dependencies
The observability umbrella vendors upstream charts under `gitops/observability/charts/*.tgz` so **Argo CD** can render without relying on live Helm repo access at sync time.
When bumping Loki / Promtail / Grafana versions, from `gitops/observability/` run:
```bash
helm dependency update
```
Commit the updated `Chart.lock` and `charts/*.tgz` with your Git change. This is **repository packaging**, not an alternative install path — deploy still happens only via Argo CD.
### OneLab `logs.path`
The OneLab chart sets `onelab.logs.path: "/logs"` in the generated configuration so application file logs match the `/logs` volume mount (see Enterprise guide §7.2).
## kubectl / credentials
If `kubectl` reports *You must be logged in*, refresh your kubeconfig (e.g. copy `/etc/rancher/k3s/k3s.yaml` from the server or re-run your auth plugin) before applying manifests.
If `kubectl` reports *You must be logged in*, refresh your kubeconfig (e.g. k3s `/etc/rancher/k3s/k3s.yaml` on the server or your auth plugin) before applying manifests.
## Helm note (Windows)
## Application configuration (`configurations.yml`)
Helm 3.19 may return empty content for `.Files.Get` on Windows; this chart uses `fromYaml (.Files.AsConfig)` as a workaround so packaged files still render correctly.
You do not need to edit [`app/configurations.yml`](../app/configurations.yml) in Git for Kubernetes. The chart renders `configurations.yml` from [`charts/onelab/files/configurations.gotmpl`](charts/onelab/files/configurations.gotmpl) into Secret **`onelab-configurations`**.
1. **Values (recommended)** — set `onelab.compliance`, `onelab.ldap`, etc. See [`values/instance-overrides.example.yaml`](values/instance-overrides.example.yaml). Add extra paths under **`spec.sources[].helm.valueFiles`** for the `gitops/charts/onelab` source (paths relative to `gitops/charts/onelab`).
2. **Bring your own Secret** — set `configuration.existingSecretName`; the Secret must contain key **`configurations.yml`**.
LDAP TLS paths in values are container paths; mount PEMs on `ldap-worker` if required.
## Ingress (web UI)
Set `ingress.enabled`, `ingress.host`, and optional TLS in values. Traffic goes to Service **`revproxy`**. On k3s, `ingress.className: traefik` matches the default controller. For cert-manager, set `ingress.tls`, `ingress.tlsSecretName`, and `ingress.certManager.clusterIssuer`; DNS for `ingress.host` must resolve before ACME runs.
## Developer note (local render)
Running **`helm template` on Windows** against some paths can return empty `.Files.Get` content; the OneLab chart uses `fromYaml (.Files.AsConfig)` where needed. **Argo CD runs on Linux** and renders the same charts in-cluster — this is a local-tooling caveat, not a second deploy path.
## Not migrated in this chart
- **Edge proxy stack** (`app/proxy/docker-compose.yml`, host 80/443) — use k3s **Traefik** / **Ingress** + **cert-manager**, or a separate DaemonSet/nginx chart.
- **Swarm-only secrets** (e.g. `ssl_passphrase`) — handle via Kubernetes Secrets or external operators.
- **Edge proxy stack** (`app/proxy/docker-compose.yml`, host 80/443 Swarm) — use **Ingress** + `revproxy` and optional cert-manager.
- **Swarm-only secrets** (e.g. `ssl_passphrase`) — use Kubernetes Secrets or external operators.

View File

@@ -1,4 +1,8 @@
# Syncs chart from Git; ensure Argo CD can clone repoURL (add credentials in Argo if private).
# Syncs OneLab app + observability (Loki/Promtail/Grafana) into namespace onelab.
# Requires Argo CD 2.6+ (spec.sources). Ensure repoURL matches your remote.
#
# If you already had this Application with spec.source only, kubectl apply may not drop
# source — see gitops/README.md (Migrating spec.source → spec.sources) and jsonpatch-multisource.json.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
@@ -8,13 +12,21 @@ metadata:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://git.luneski.fr/luneski/onelab-k8s.git
targetRevision: main
path: gitops/charts/onelab
helm:
valueFiles:
- ../../values/k3s-example.yaml
sources:
- repoURL: https://git.luneski.fr/luneski/onelab-k8s.git
targetRevision: main
path: gitops/charts/onelab
helm:
releaseName: onelab
valueFiles:
- ../../values/k3s-example.yaml
- repoURL: https://git.luneski.fr/luneski/onelab-k8s.git
targetRevision: main
path: gitops/observability
helm:
releaseName: onelab-obs
valueFiles:
- values.yaml
destination:
server: https://kubernetes.default.svc
namespace: onelab

View File

@@ -0,0 +1,23 @@
[
{"op": "remove", "path": "/spec/source"},
{"op": "add", "path": "/spec/sources", "value": [
{
"repoURL": "https://git.luneski.fr/luneski/onelab-k8s.git",
"targetRevision": "main",
"path": "gitops/charts/onelab",
"helm": {
"releaseName": "onelab",
"valueFiles": ["../../values/k3s-example.yaml"]
}
},
{
"repoURL": "https://git.luneski.fr/luneski/onelab-k8s.git",
"targetRevision": "main",
"path": "gitops/observability",
"helm": {
"releaseName": "onelab-obs",
"valueFiles": ["values.yaml"]
}
}
]}
]

View File

@@ -2,6 +2,7 @@
onelab:
domain: {{ .Values.onelab.domain | quote }}
logs:
path: "/logs"
level: info
assets:
purge: 1d
@@ -41,6 +42,15 @@ onelab:
remember_me: true
lab:
creation_policy: many
{{- if .Values.onelab.compliance.enabled }}
compliance:
require_electronic_signature: {{ .Values.onelab.compliance.requireElectronicSignature }}
execution_operator_restriction_policy: {{ .Values.onelab.compliance.executionOperatorRestrictionPolicy | quote }}
execution_admin_expert_restriction_policy: {{ .Values.onelab.compliance.executionAdminExpertRestrictionPolicy | quote }}
prevent_csv_import: {{ .Values.onelab.compliance.preventCsvImport }}
prevent_manual_metadata_edit: {{ .Values.onelab.compliance.preventManualMetadataEdit }}
device_restart: {{ .Values.onelab.compliance.deviceRestart }}
{{- end }}
signup: false
{{- if .Values.onelab.intercom.appid }}
intercom:
@@ -56,7 +66,39 @@ onelab:
maxtries: 3
timeout: 60
ldap:
enabled: {{ .Values.features.ldapWorker }}
enabled: {{ if or .Values.onelab.ldap.enabled .Values.features.ldapWorker }}true{{ else }}false{{ end }}
{{- if or .Values.onelab.ldap.enabled .Values.features.ldapWorker }}
{{- if .Values.onelab.ldap.timeout }}
timeout: {{ .Values.onelab.ldap.timeout | int }}
{{- end }}
{{- if .Values.onelab.ldap.encryption }}
encryption: {{ .Values.onelab.ldap.encryption | quote }}
{{- end }}
{{- if .Values.onelab.ldap.policy }}
policy: {{ .Values.onelab.ldap.policy | quote }}
{{- end }}
{{- if kindIs "bool" .Values.onelab.ldap.verifyCertificates }}
verify_certificates: {{ .Values.onelab.ldap.verifyCertificates }}
{{- end }}
{{- if or .Values.onelab.ldap.tlsCaPath .Values.onelab.ldap.tlsCertPath .Values.onelab.ldap.tlsKeyPath .Values.onelab.ldap.tlsCiphers .Values.onelab.ldap.tlsSslVersion }}
tls:
{{- if .Values.onelab.ldap.tlsCaPath }}
ca: {{ .Values.onelab.ldap.tlsCaPath | quote }}
{{- end }}
{{- if .Values.onelab.ldap.tlsCertPath }}
cert: {{ .Values.onelab.ldap.tlsCertPath | quote }}
{{- end }}
{{- if .Values.onelab.ldap.tlsKeyPath }}
key: {{ .Values.onelab.ldap.tlsKeyPath | quote }}
{{- end }}
{{- if .Values.onelab.ldap.tlsCiphers }}
ciphers: {{ .Values.onelab.ldap.tlsCiphers | quote }}
{{- end }}
{{- if .Values.onelab.ldap.tlsSslVersion }}
ssl_version: {{ .Values.onelab.ldap.tlsSslVersion | quote }}
{{- end }}
{{- end }}
{{- end }}
services:
db:
host: db

View File

@@ -23,3 +23,15 @@ app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
{{- define "onelab.dockerconfigjson" -}}
{{- $server := .Values.registry.server -}}
{{- $user := .Values.registry.username -}}
{{- $pass := .Values.registry.password -}}
{{- $auth := printf "%s:%s" $user $pass | b64enc -}}
{{- $entry := dict "username" $user "password" $pass "auth" $auth -}}
{{- dict "auths" (dict $server $entry) | toJson -}}
{{- end }}
{{- define "onelab.configurationSecretName" -}}
{{- .Values.configuration.existingSecretName | default "onelab-configurations" }}
{{- end }}

View File

@@ -1,5 +1,5 @@
{{- $root := . }}
{{- if .Values.features.ldapWorker }}
{{- if or .Values.onelab.ldap.enabled .Values.features.ldapWorker }}
---
apiVersion: apps/v1
kind: Deployment
@@ -43,7 +43,7 @@ spec:
volumes:
- name: configurations
secret:
secretName: onelab-configurations
secretName: {{ include "onelab.configurationSecretName" $root }}
{{- if eq $root.Values.persistence.mode "hostPath" }}
- name: logs
hostPath:
@@ -98,7 +98,7 @@ spec:
volumes:
- name: configurations
secret:
secretName: onelab-configurations
secretName: {{ include "onelab.configurationSecretName" $root }}
{{- if eq $root.Values.persistence.mode "hostPath" }}
- name: logs
hostPath:

View File

@@ -0,0 +1,37 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: onelab-revproxy
labels:
{{- include "onelab.labels" . | nindent 4 }}
annotations:
argocd.argoproj.io/sync-wave: {{ .Values.syncWaves.apps | quote }}
{{- if .Values.ingress.certManager.clusterIssuer }}
cert-manager.io/cluster-issuer: {{ .Values.ingress.certManager.clusterIssuer | quote }}
{{- end }}
{{- with .Values.ingress.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className | quote }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
- hosts:
- {{ .Values.ingress.host | quote }}
secretName: {{ if .Values.ingress.tlsSecretName }}{{ .Values.ingress.tlsSecretName | quote }}{{ else }}{{ printf "%s-tls" .Release.Name | quote }}{{ end }}
{{- end }}
rules:
- host: {{ .Values.ingress.host | quote }}
http:
paths:
- path: {{ .Values.ingress.path | quote }}
pathType: {{ .Values.ingress.pathType | quote }}
backend:
service:
name: revproxy
port:
name: http
{{- end }}

View File

@@ -1,3 +1,4 @@
{{- if not .Values.configuration.existingSecretName }}
{{- $cfg := fromYaml (.Files.AsConfig) }}
apiVersion: v1
kind: Secret
@@ -11,3 +12,4 @@ type: Opaque
stringData:
configurations.yml: |
{{- tpl (index $cfg "configurations.gotmpl") . | nindent 4 }}
{{- end }}

View File

@@ -0,0 +1,13 @@
{{- if .Values.registry.createPullSecret }}
apiVersion: v1
kind: Secret
metadata:
name: {{ .Values.registry.pullSecretName }}
labels:
{{- include "onelab.labels" . | nindent 4 }}
annotations:
argocd.argoproj.io/sync-wave: {{ .Values.syncWaves.registry | quote }}
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: {{ include "onelab.dockerconfigjson" . | b64enc }}
{{- end }}

View File

@@ -20,6 +20,12 @@ spec:
app.kubernetes.io/component: postgres
app.kubernetes.io/name: {{ include "onelab.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
annotations:
{{- if .Values.registry.createPullSecret }}
checksum/docker-registry: {{ include "onelab.dockerconfigjson" . | sha256sum | quote }}
{{- else if not (empty .Values.imagePullSecrets) }}
checksum/image-pull-secrets: {{ .Values.imagePullSecrets | toJson | sha256sum | quote }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:

View File

@@ -20,6 +20,12 @@ spec:
app.kubernetes.io/component: rabbitmq
app.kubernetes.io/name: {{ include "onelab.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
annotations:
{{- if .Values.registry.createPullSecret }}
checksum/docker-registry: {{ include "onelab.dockerconfigjson" . | sha256sum | quote }}
{{- else if not (empty .Values.imagePullSecrets) }}
checksum/image-pull-secrets: {{ .Values.imagePullSecrets | toJson | sha256sum | quote }}
{{- end }}
spec:
hostname: onelab
{{- with .Values.imagePullSecrets }}

View File

@@ -76,7 +76,7 @@ spec:
{{- if .config }}
- name: configurations
secret:
secretName: onelab-configurations
secretName: {{ include "onelab.configurationSecretName" $root }}
{{- end }}
{{- if eq $root.Values.persistence.mode "hostPath" }}
{{- if has "logs" .mounts }}

View File

@@ -3,6 +3,11 @@
nameOverride: ""
fullnameOverride: ""
# If non-empty, workloads mount this Secret instead of chart-generated onelab-configurations.
# Secret must contain key `configurations.yml`. Chart will NOT create onelab-configurations.
configuration:
existingSecretName: ""
images:
registry: hub.andrewalliance.com/releases
tag: "1.27.0"
@@ -18,6 +23,14 @@ images:
imagePullSecrets: []
# - name: hub-andrewalliance
# Same defaults as app/playbooks/tasks/manage-images.yml (docker login before pull).
registry:
createPullSecret: false
pullSecretName: hub-andrewalliance
server: hub.andrewalliance.com
username: public
password: Andrew01..Release
# hostPath: matches typical single-node Swarm-style install (shared /data and /logs).
# Use persistence.mode: pvc + a ReadWriteMany class for multi-node shared storage.
persistence:
@@ -33,7 +46,7 @@ persistence:
postgresql:
auth:
password: "changeme-use-strong-password"
password: "DBPasswordPlaceholder"
resources: {}
redis:
@@ -50,6 +63,7 @@ rabbitmq:
fullchain: ""
syncWaves:
registry: "-5"
postgres: "-3"
statefulDeps: "-2"
apps: "0"
@@ -59,14 +73,35 @@ onelab:
mailer:
noreply: "no-reply@andrewalliance.com"
secrets:
authTokenKey: "replace-auth-token-key"
monitoringToken: "replace-monitoring-token"
rabbitToken: "replace-rabbit-token"
authTokenKey: "TokenAuthPlaceholder"
monitoringToken: "TokenMonitoringPlaceholder"
rabbitToken: "TokenRabbitPlaceholder"
# Mirrors app/configurations.yml params.compliance (enable without editing app/).
compliance:
enabled: false
requireElectronicSignature: true
executionOperatorRestrictionPolicy: "reviewed"
executionAdminExpertRestrictionPolicy: "reviewed"
preventCsvImport: true
preventManualMetadataEdit: true
deviceRestart: true
# Set enabled: true to turn on LDAP in configurations.yml and deploy ldap-worker (or use features.ldapWorker).
ldap:
enabled: false
timeout: ""
encryption: ""
policy: ""
tlsCaPath: ""
tlsCertPath: ""
tlsKeyPath: ""
tlsCiphers: ""
tlsSslVersion: ""
intercom:
appid: ""
secret: "replace-intercom-secret"
appid: "zxvgsagz"
secret: "QUw2jEV8utIpe9DeYjOqBjhBY9VxjXddKUCISUNu"
features:
# Deprecated for LDAP: prefer onelab.ldap.enabled (either enables ldap-worker + ldap.enabled in config).
ldapWorker: false
mailerWorker: false
@@ -78,6 +113,20 @@ revproxy:
nodePort: 30080
ipv6Listen: true
# HTTP routing to internal nginx (revproxy). On k3s, set className: traefik (default controller).
ingress:
enabled: false
className: ""
host: onelab.local
path: /
pathType: Prefix
annotations: {}
tls: false
tlsSecretName: ""
certManager:
# When set, adds cert-manager.io/cluster-issuer annotation (TLS secret is created automatically).
clusterIssuer: ""
# Replica counts (api.apidevice etc. override defaults in templates/workloads.yaml via this map)
replicas:
api: 2

View File

@@ -0,0 +1,12 @@
dependencies:
- name: loki
repository: https://grafana.github.io/helm-charts
version: 6.55.0
- name: promtail
repository: https://grafana.github.io/helm-charts
version: 6.17.1
- name: grafana
repository: https://grafana.github.io/helm-charts
version: 10.5.15
digest: sha256:5b34192a8db9d940587777fbc62a13503c21217da814308654ce73fca2ed5d56
generated: "2026-03-20T11:06:47.9376325+01:00"

View File

@@ -0,0 +1,16 @@
apiVersion: v2
name: onelab-observability
description: Loki + Promtail + Grafana for OneLab (same Argo Application as app chart via multi-source).
type: application
version: 0.1.0
appVersion: "1.0"
dependencies:
- name: loki
version: 6.55.0
repository: https://grafana.github.io/helm-charts
- name: promtail
version: 6.17.1
repository: https://grafana.github.io/helm-charts
- name: grafana
version: 10.5.15
repository: https://grafana.github.io/helm-charts

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,205 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {"type": "grafana", "uid": "-- Grafana --"},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {"type": "loki", "uid": "loki"},
"fieldConfig": {
"defaults": {
"color": {"mode": "thresholds"},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{"color": "blue", "value": null}]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {"h": 5, "w": 8, "x": 0, "y": 0},
"id": 1,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false},
"textMode": "auto"
},
"targets": [
{
"datasource": {"type": "loki", "uid": "loki"},
"editorMode": "code",
"expr": "sum(count_over_time({namespace=\"onelab\", component=~\"$component\"} |~ \"$filter\" [$__range]))",
"queryType": "instant",
"refId": "A"
}
],
"title": "Total lines (namespace onelab, matches line filter)",
"type": "stat"
},
{
"datasource": {"type": "loki", "uid": "loki"},
"fieldConfig": {
"defaults": {
"color": {"mode": "thresholds"},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "green", "value": null},
{"color": "orange", "value": 1}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {"h": 5, "w": 8, "x": 8, "y": 0},
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false},
"textMode": "auto"
},
"targets": [
{
"datasource": {"type": "loki", "uid": "loki"},
"editorMode": "code",
"expr": "sum(count_over_time({namespace=\"onelab\", component=~\"$component\"} |~ \"$filter\" |~ \"(?i)(\\\\[ERROR\\\\]|\\\\berror\\\\b|\\\\sERROR\\\\s)\" [$__range]))",
"queryType": "instant",
"refId": "A"
}
],
"title": "~ Error-like lines (heuristic)",
"type": "stat"
},
{
"datasource": {"type": "loki", "uid": "loki"},
"fieldConfig": {
"defaults": {
"color": {"mode": "thresholds"},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "green", "value": null},
{"color": "yellow", "value": 1}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {"h": 5, "w": 8, "x": 16, "y": 0},
"id": 3,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false},
"textMode": "auto"
},
"targets": [
{
"datasource": {"type": "loki", "uid": "loki"},
"editorMode": "code",
"expr": "sum(count_over_time({namespace=\"onelab\", component=~\"$component\"} |~ \"$filter\" |~ \"(?i)(\\\\[WARN\\\\]|\\\\bwarn(ing)?\\\\b|\\\\sWARN\\\\s)\" [$__range]))",
"queryType": "instant",
"refId": "A"
}
],
"title": "~ Warning-like lines (heuristic)",
"type": "stat"
},
{
"datasource": {"type": "loki", "uid": "loki"},
"gridPos": {"h": 16, "w": 24, "x": 0, "y": 5},
"id": 4,
"options": {
"dedupStrategy": "none",
"enableLogDetails": true,
"prettifyLogMessage": false,
"showCommonLabels": false,
"showLabels": true,
"showTime": true,
"sortOrder": "Descending",
"wrapLogMessage": true
},
"targets": [
{
"datasource": {"type": "loki", "uid": "loki"},
"editorMode": "code",
"expr": "{namespace=\"onelab\", component=~\"$component\"} |~ \"$filter\"",
"queryType": "range",
"refId": "A"
}
],
"title": "OneLab logs — use Component + Line filter (regex)",
"type": "logs"
}
],
"refresh": "30s",
"schemaVersion": 39,
"tags": ["onelab", "loki"],
"templating": {
"list": [
{
"allValue": ".*",
"current": {"selected": true, "text": "All", "value": "$__all"},
"datasource": {"type": "loki", "uid": "loki"},
"definition": "label_values({namespace=\"onelab\"}, component)",
"hide": 0,
"includeAll": true,
"label": "Component",
"multi": true,
"name": "component",
"options": [],
"query": "label_values({namespace=\"onelab\"}, component)",
"refresh": 2,
"regex": "",
"sort": 1,
"type": "query"
},
{
"current": {"selected": true, "text": ".*", "value": ".*"},
"hide": 0,
"label": "Line filter (regex)",
"name": "filter",
"options": [
{"selected": true, "text": ".*", "value": ".*"}
],
"query": ".*",
"type": "textbox"
}
]
},
"time": {"from": "now-1h", "to": "now"},
"timepicker": {},
"timezone": "browser",
"title": "OneLab logs",
"uid": "onelab-logs",
"version": 1,
"weekStart": ""
}

View File

@@ -0,0 +1,14 @@
{{- if .Values.grafana.sidecar.dashboards.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ printf "%s-dashboard-onelab-logs" .Release.Name | trunc 63 | trimSuffix "-" }}
namespace: {{ .Release.Namespace }}
labels:
grafana_dashboard: "1"
annotations:
argocd.argoproj.io/sync-wave: "0"
data:
onelab-logs.json: |-
{{ .Files.Get "dashboards/onelab-logs.json" | nindent 4 }}
{{- end }}

View File

@@ -0,0 +1,36 @@
{{- if .Values.grafanaOnelabIngress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grafana-onelab
namespace: {{ .Release.Namespace }}
labels:
app.kubernetes.io/name: grafana-onelab
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: observability
annotations:
argocd.argoproj.io/sync-wave: "0"
cert-manager.io/cluster-issuer: {{ .Values.grafanaOnelabIngress.clusterIssuer | quote }}
{{- with .Values.grafanaOnelabIngress.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
ingressClassName: {{ .Values.grafanaOnelabIngress.className | quote }}
{{- if .Values.grafanaOnelabIngress.tls }}
tls:
- hosts:
- {{ .Values.grafanaOnelabIngress.host | quote }}
secretName: {{ .Values.grafanaOnelabIngress.tlsSecretName | quote }}
{{- end }}
rules:
- host: {{ .Values.grafanaOnelabIngress.host | quote }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ printf "%s-grafana" .Release.Name }}
port:
number: {{ .Values.grafanaOnelabIngress.servicePort }}
{{- end }}

View File

@@ -0,0 +1,149 @@
# Umbrella chart: Loki (SingleBinary + filesystem) + Promtail + Grafana.
# Keep hostPath below in sync with persistence.hostPath.logs in gitops/values/k3s-example.yaml.
loki:
deploymentMode: SingleBinary
loki:
auth_enabled: false
commonConfig:
replication_factor: 1
storage:
type: filesystem
schemaConfig:
configs:
- from: "2024-04-01"
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: loki_index_
period: 24h
limits_config:
retention_period: 168h
ingestion_rate_mb: 16
ingestion_burst_size_mb: 32
singleBinary:
replicas: 1
persistence:
enabled: true
size: 10Gi
backend:
replicas: 0
read:
replicas: 0
write:
replicas: 0
ingester:
replicas: 0
querier:
replicas: 0
queryFrontend:
replicas: 0
queryScheduler:
replicas: 0
distributor:
replicas: 0
compactor:
replicas: 0
indexGateway:
replicas: 0
bloomCompactor:
replicas: 0
bloomGateway:
replicas: 0
ruler:
replicas: 0
minio:
enabled: false
lokiCanary:
enabled: false
test:
enabled: false
chunksCache:
enabled: false
resultsCache:
enabled: false
promtail:
config:
clients:
- url: http://{{ .Release.Name }}-loki-gateway.{{ .Release.Namespace }}.svc.cluster.local/loki/api/v1/push
snippets:
# Only ingest pod logs from namespace onelab (Explore / Loki stay focused on OneLab).
extraRelabelConfigs:
- action: keep
source_labels:
- __meta_kubernetes_namespace
regex: onelab
extraScrapeConfigs: |
- job_name: onelab-host-log-files
static_configs:
- targets:
- localhost
labels:
job: onelab-files
namespace: onelab
component: host-logs
__path__: /onelab-host-logs/**/*
extraVolumes:
- name: onelab-host-logs
hostPath:
path: /opt/onelab/logs
type: DirectoryOrCreate
extraVolumeMounts:
- name: onelab-host-logs
mountPath: /onelab-host-logs
readOnly: true
# Named Ingress grafana-onelab (templates/ingress-grafana-onelab.yaml). Grafana subchart ingress is disabled.
grafanaOnelabIngress:
enabled: true
className: traefik
host: grafana.k8s.selair.it
tls: true
tlsSecretName: grafana-tls-k8s-selair
clusterIssuer: letsencrypt-prod
servicePort: 80
annotations: {}
grafana:
adminUser: admin
adminPassword: changeme
# Root+CHOWN init breaks under Pod Security / restricted policies (k3s). fsGroup:472 on the pod is enough for most PVCs.
initChownData:
enabled: false
# Load dashboards from ConfigMaps labeled grafana_dashboard (see templates/configmap-dashboard-onelab-logs.yaml).
sidecar:
dashboards:
enabled: true
label: grafana_dashboard
folder: /tmp/dashboards
provider:
foldersFromFilesStructure: false
allowUiUpdates: true
datasources:
enabled: false
persistence:
enabled: true
size: 2Gi
service:
type: ClusterIP
# Required when served behind Ingress (redirects, OAuth callbacks).
grafana.ini:
server:
domain: grafana.k8s.selair.it
root_url: https://grafana.k8s.selair.it/
ingress:
enabled: false
datasources:
datasources.yaml:
apiVersion: 1
datasources:
- name: Loki
type: loki
uid: loki
url: http://{{ .Release.Name }}-loki-gateway.{{ .Release.Namespace }}.svc.cluster.local
access: proxy
isDefault: true
jsonData:
maxLines: 1000

View File

@@ -0,0 +1,37 @@
# Copy to a private file (e.g. gitops/values/private-k3s.yaml, gitignored) or merge into gitops/values/k3s-example.yaml.
#
# Argo CD: under spec.sources, for the source with path gitops/charts/onelab, add another path to helm.valueFiles
# (paths are relative to that chart directory), e.g.:
# - ../../values/k3s-example.yaml
# - ../../values/private-k3s.yaml
onelab:
compliance:
enabled: true
# Optional tweaks (defaults match chart values.yaml):
# requireElectronicSignature: true
# executionOperatorRestrictionPolicy: "reviewed"
# executionAdminExpertRestrictionPolicy: "reviewed"
# preventCsvImport: true
# preventManualMetadataEdit: true
# deviceRestart: true
ldap:
enabled: true
# timeout: 30
# encryption: "start_tls"
# policy: "your-policy"
# verifyCertificates: true
# Paths inside the ldap-worker container (mount certs via extraVolumes if needed):
# tlsCaPath: "/ldap/ca.crt"
# tlsCertPath: "/ldap/client.crt"
# tlsKeyPath: "/ldap/client.key"
# tlsCiphers: ""
# tlsSslVersion: ""
# Alternative: supply the full YAML yourself (bypasses chart templates in configurations.gotmpl for those keys).
# 1. kubectl create secret generic onelab-configurations-custom -n onelab \
# --from-file=configurations.yml=./my-configurations.yml
# 2. Set in values:
# configuration:
# existingSecretName: onelab-configurations-custom

View File

@@ -1,10 +1,16 @@
# k3s / Argo CD overlay (private Git — rotate secrets if this file is ever made public).
# Add image pull credentials when using hub.andrewalliance.com:
# kubectl create secret docker-registry hub-andrewalliance -n onelab \
# --docker-server=hub.andrewalliance.com --docker-username=... --docker-password=...
# then set imagePullSecrets below.
# Aligned with Swarm installer defaults:
# - Registry: app/playbooks/tasks/manage-images.yml (user public, password Andrew01..Release)
# - App config sample: app/configurations.yml (placeholders + intercom block)
imagePullSecrets: []
registry:
createPullSecret: true
pullSecretName: hub-andrewalliance
server: hub.andrewalliance.com
username: public
password: Andrew01..Release
imagePullSecrets:
- name: hub-andrewalliance
persistence:
mode: hostPath
@@ -14,18 +20,32 @@ persistence:
postgresql:
auth:
password: "9daLpcV7vKS1zXUElQRO5h4u"
password: "DBPasswordPlaceholder"
onelab:
domain: "https://onelab.example.com"
# Public URL (must match ingress host + scheme).
domain: "https://onelab.k8s.selair.it"
secrets:
authTokenKey: "ntH0Yd3AcsqwMu7ah8xLbWFS4BK5GUmi"
monitoringToken: "Cj4ix7wdg8XPIsDAFENKRTmh6lkvBLZp"
rabbitToken: "GmSWRv14PXZuyM5QDgb8wpxk0dh7F6IJ"
authTokenKey: "TokenAuthPlaceholder"
monitoringToken: "TokenMonitoringPlaceholder"
rabbitToken: "TokenRabbitPlaceholder"
intercom:
appid: ""
secret: ""
appid: "zxvgsagz"
secret: "QUw2jEV8utIpe9DeYjOqBjhBY9VxjXddKUCISUNu"
# ClusterIP keeps traffic via Ingress only; use NodePort instead if you need direct node:port access.
revproxy:
serviceType: NodePort
nodePort: 30080
serviceType: ClusterIP
ingress:
enabled: true
className: traefik
host: onelab.k8s.selair.it
path: /
pathType: Prefix
tls: true
# cert-manager writes the certificate into this Secret in the release namespace
tlsSecretName: onelab-tls-k8s-selair
certManager:
clusterIssuer: letsencrypt-prod
annotations: {}