Compare commits

...

9 commits

Author SHA1 Message Date
286a6e70a8 C2(upgrade-grafana): impl fix grafana tarball extract path
The Grafana OSS tarball extracts to grafana-<version> (no v prefix),
not grafana-v<version>. Fix the mv command to match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 17:48:42 -08:00
6566cad5b2 C2(upgrade-grafana): close kustomize-grafana-deployment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 17:48:32 -08:00
892d5bc4af C2(upgrade-grafana): impl kustomize-grafana-deployment
Replace Helm chart with plain kustomize manifests:
- deployment.yaml: Grafana 12.3.3 (home-built) + k8s-sidecar + init container
- configmap.yaml: grafana.ini (Authentik OIDC, datasources, paths)
- service.yaml, pvc.yaml, serviceaccount.yaml, rbac.yaml
- ArgoCD app converted from Helm multi-source to single kustomize source
- Removed Helm values.yaml

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 17:48:32 -08:00
cafa44a817 C2(upgrade-grafana): close build-grafana-container
Home-built Grafana 12.3.3 container is ready. Dockerfile builds from
Alpine 3.22 + official OSS tarball, verified via dagger and
container-version-check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 17:48:32 -08:00
d8f7ec4a19 C2(upgrade-grafana): impl build-grafana-container
Add home-built Grafana 12.3.3 container image based on Alpine 3.22
with pre-built OSS tarball from dl.grafana.com. Uses UID 472 for PVC
compatibility with the official image, standard Grafana paths, and
multi-arch support via TARGETPLATFORM detection.

Update service-versions.yaml to track 12.3.3.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 17:48:32 -08:00
b2c3dc527f C2(upgrade-grafana): plan document grafana tarball path lesson
Discovered during build: the Grafana OSS tarball extracts to
grafana-<version>, not grafana-v<version>. The Dockerfile mv
command must match this naming.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 17:48:32 -08:00
629a5d4f30 C2(upgrade-grafana): plan add branch field to goal card
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 17:48:32 -08:00
9b419abf24 Update RUNNER_LABELS to use runner-job-image:v0.19.11-4c5e0f0
Now that the image is built under the new name, point the forgejo
runner at it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 17:47:14 -08:00
4c5e0f0d16 Rename containers/forgejo-runner to runner-job-image
All checks were successful
Build Container (Nix) / detect (push) Successful in 2s
Build Container / detect (push) Successful in 2s
Build Container (Nix) / build (runner-job-image) (push) Successful in 2s
Build Container / build (runner-job-image) (push) Successful in 1m42s
The forgejo-runner container is the CI job execution environment (Dagger,
ArgoCD CLI, etc.), not the runner daemon itself. Rename to runner-job-image
to fix the version-check false positive (Dagger 0.19.11 vs daemon 12.7.0)
and clarify the distinction.

RUNNER_LABELS still references the old image name — will update after
building the image under the new name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 17:44:51 -08:00
18 changed files with 470 additions and 132 deletions

View file

@ -1,7 +1,5 @@
# Grafana - Dashboards & Observability
#
# Chart mirrored from https://github.com/grafana/helm-charts to forge
#
# Before syncing, create the admin password secret:
# kubectl create namespace monitoring
# op inject -i argocd/manifests/grafana-config/secret-admin.yaml.tpl | kubectl apply -f -
@ -12,19 +10,10 @@ metadata:
namespace: argocd
spec:
project: default
sources:
# Helm chart from forge mirror (SSH via egress)
- repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/grafana-helm-charts.git
targetRevision: grafana-8.8.2
path: charts/grafana
helm:
releaseName: grafana
valueFiles:
- $values/argocd/manifests/grafana/values.yaml
# Values from our git repo
- repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
ref: values
source:
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/grafana
destination:
server: https://kubernetes.default.svc
namespace: monitoring

View file

@ -29,7 +29,7 @@ spec:
- name: RUNNER_NAME
value: "k8s-runner"
- name: RUNNER_LABELS
value: "k8s:docker://registry.ops.eblu.me/blumeops/forgejo-runner:v0.19.11-96a2d42"
value: "k8s:docker://registry.ops.eblu.me/blumeops/runner-job-image:v0.19.11-4c5e0f0"
command:
- /bin/sh
- -c

View file

@ -0,0 +1,98 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana
namespace: monitoring
labels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
data:
grafana.ini: |
[analytics]
check_for_updates = false
reporting_enabled = false
[auth.generic_oauth]
allow_sign_up = true
api_url = https://authentik.ops.eblu.me/application/o/userinfo/
auth_url = https://authentik.ops.eblu.me/application/o/authorize/
auto_login = false
client_id = grafana
client_secret = $__env{GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET}
enabled = true
name = Authentik
role_attribute_path = 'Admin'
scopes = openid profile email
token_url = https://authentik.ops.eblu.me/application/o/token/
[log]
mode = console
[paths]
data = /var/lib/grafana/
logs = /var/log/grafana
plugins = /var/lib/grafana/plugins
provisioning = /etc/grafana/provisioning
[security]
allow_embedding = false
[server]
root_url = https://grafana.ops.eblu.me
datasources.yaml: |
apiVersion: 1
datasources:
- access: proxy
editable: false
isDefault: true
name: Prometheus
orgId: 1
type: prometheus
uid: prometheus
url: http://prometheus.monitoring.svc.cluster.local:9090
- access: proxy
editable: false
name: Loki
orgId: 1
type: loki
uid: loki
url: http://loki.monitoring.svc.cluster.local:3100
- access: proxy
database: teslamate
editable: false
jsonData:
connMaxLifetime: 14400
maxIdleConns: 2
maxOpenConns: 5
sslmode: disable
name: TeslaMate
orgId: 1
secureJsonData:
password: $TESLAMATE_DB_PASSWORD
type: postgres
uid: TeslaMate
url: blumeops-pg-rw.databases.svc.cluster.local:5432
user: teslamate
---
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-config-dashboards
namespace: monitoring
labels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
data:
provider.yaml: |
apiVersion: 1
providers:
- name: 'sidecarProvider'
orgId: 1
type: file
disableDeletion: false
allowUiUpdates: false
updateIntervalSeconds: 30
options:
foldersFromFilesStructure: true
path: /tmp/dashboards

View file

@ -0,0 +1,176 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: grafana
namespace: monitoring
labels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
spec:
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
strategy:
type: RollingUpdate
template:
metadata:
labels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
annotations:
kubectl.kubernetes.io/default-container: grafana
spec:
automountServiceAccountToken: true
serviceAccountName: grafana
securityContext:
fsGroup: 472
runAsGroup: 472
runAsNonRoot: true
runAsUser: 472
initContainers:
- name: init-chown-data
image: docker.io/library/busybox:1.31.1
imagePullPolicy: IfNotPresent
command: ["chown", "-R", "472:472", "/var/lib/grafana"]
securityContext:
runAsNonRoot: false
runAsUser: 0
capabilities:
add: ["CHOWN"]
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: storage
mountPath: /var/lib/grafana
containers:
# Dashboard sidecar - watches ConfigMaps with grafana_dashboard=1
- name: grafana-sc-dashboard
image: quay.io/kiwigrid/k8s-sidecar:1.28.0
imagePullPolicy: IfNotPresent
env:
- name: METHOD
value: WATCH
- name: LABEL
value: grafana_dashboard
- name: LABEL_VALUE
value: "1"
- name: FOLDER
value: /tmp/dashboards
- name: RESOURCE
value: both
- name: FOLDER_ANNOTATION
value: grafana_folder
- name: REQ_USERNAME
valueFrom:
secretKeyRef:
name: grafana-admin
key: admin-user
- name: REQ_PASSWORD
valueFrom:
secretKeyRef:
name: grafana-admin
key: admin-password
- name: REQ_URL
value: http://localhost:3000/api/admin/provisioning/dashboards/reload
- name: REQ_METHOD
value: POST
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: sc-dashboard-volume
mountPath: /tmp/dashboards
# Grafana
- name: grafana
image: registry.ops.eblu.me/blumeops/grafana:v12.3.3-b1ea762
imagePullPolicy: IfNotPresent
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: GF_SECURITY_ADMIN_USER
valueFrom:
secretKeyRef:
name: grafana-admin
key: admin-user
- name: GF_SECURITY_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: grafana-admin
key: admin-password
- name: GF_PATHS_DATA
value: /var/lib/grafana/
- name: GF_PATHS_LOGS
value: /var/log/grafana
- name: GF_PATHS_PLUGINS
value: /var/lib/grafana/plugins
- name: GF_PATHS_PROVISIONING
value: /etc/grafana/provisioning
envFrom:
- secretRef:
name: grafana-teslamate-datasource
optional: true
- secretRef:
name: grafana-authentik-oauth
optional: true
ports:
- name: http
containerPort: 3000
protocol: TCP
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 60
timeoutSeconds: 30
failureThreshold: 10
readinessProbe:
httpGet:
path: /api/health
port: 3000
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: config
mountPath: /etc/grafana/grafana.ini
subPath: grafana.ini
- name: config
mountPath: /etc/grafana/provisioning/datasources/datasources.yaml
subPath: datasources.yaml
- name: storage
mountPath: /var/lib/grafana
- name: sc-dashboard-volume
mountPath: /tmp/dashboards
- name: sc-dashboard-provider
mountPath: /etc/grafana/provisioning/dashboards/sc-dashboardproviders.yaml
subPath: provider.yaml
volumes:
- name: config
configMap:
name: grafana
- name: storage
persistentVolumeClaim:
claimName: grafana
- name: sc-dashboard-volume
emptyDir: {}
- name: sc-dashboard-provider
configMap:
name: grafana-config-dashboards

View file

@ -0,0 +1,12 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: monitoring
resources:
- serviceaccount.yaml
- configmap.yaml
- pvc.yaml
- deployment.yaml
- service.yaml
- rbac.yaml

View file

@ -0,0 +1,14 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: grafana
namespace: monitoring
labels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

View file

@ -0,0 +1,54 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: grafana-clusterrole
labels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
rules:
- apiGroups: [""]
resources: ["configmaps", "secrets"]
verbs: ["get", "watch", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: grafana-clusterrolebinding
labels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: grafana-clusterrole
subjects:
- kind: ServiceAccount
name: grafana
namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: grafana
namespace: monitoring
labels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
rules: []
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: grafana
namespace: monitoring
labels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: grafana
subjects:
- kind: ServiceAccount
name: grafana
namespace: monitoring

View file

@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
name: grafana
namespace: monitoring
labels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
spec:
type: ClusterIP
ports:
- name: http
port: 80
targetPort: 3000
protocol: TCP
selector:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana

View file

@ -0,0 +1,9 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: grafana
namespace: monitoring
labels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
automountServiceAccountToken: false

View file

@ -1,108 +0,0 @@
# Grafana Helm values for blumeops
# Chart: https://github.com/grafana/helm-charts/tree/main/charts/grafana
# Admin credentials from pre-created secret
# Secret must exist before deploying - see grafana-config/README.md
admin:
existingSecret: grafana-admin
userKey: admin-user
passwordKey: admin-password
# Environment variables from secrets (for datasource credentials)
envFromSecrets:
- name: grafana-teslamate-datasource
optional: true
- name: grafana-authentik-oauth
optional: true
# Persistence with PVC for SQLite database
persistence:
enabled: true
type: pvc
size: 1Gi
accessModes:
- ReadWriteOnce
# Grafana configuration via grafana.ini
grafana.ini:
server:
root_url: https://grafana.ops.eblu.me
security:
# Embedding disabled - iframe approach didn't work well for Homepage
allow_embedding: false
auth.generic_oauth:
enabled: true
name: Authentik
client_id: grafana
client_secret: $__env{GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET}
scopes: openid profile email
auth_url: https://authentik.ops.eblu.me/application/o/authorize/
token_url: https://authentik.ops.eblu.me/application/o/token/
api_url: https://authentik.ops.eblu.me/application/o/userinfo/
allow_sign_up: true
role_attribute_path: "'Admin'"
auto_login: false
analytics:
check_for_updates: false
reporting_enabled: false
# Datasources - point to k8s-internal services
datasources:
datasources.yaml:
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
orgId: 1
uid: prometheus
url: http://prometheus.monitoring.svc.cluster.local:9090
isDefault: true
editable: false
- name: Loki
type: loki
access: proxy
orgId: 1
uid: loki
url: http://loki.monitoring.svc.cluster.local:3100
editable: false
- name: TeslaMate
type: postgres
access: proxy
orgId: 1
uid: TeslaMate
url: blumeops-pg-rw.databases.svc.cluster.local:5432
database: teslamate
user: teslamate
editable: false
jsonData:
sslmode: disable
maxOpenConns: 5
maxIdleConns: 2
connMaxLifetime: 14400
secureJsonData:
password: $TESLAMATE_DB_PASSWORD
# Dashboard provisioning - sidecar watches for ConfigMaps with label
sidecar:
dashboards:
enabled: true
label: grafana_dashboard
labelValue: "1"
folderAnnotation: grafana_folder
provider:
foldersFromFilesStructure: true
# Service configuration (Ingress will handle external access)
service:
type: ClusterIP
port: 80
# Resource limits for minikube
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"

View file

@ -0,0 +1,63 @@
ARG CONTAINER_APP_VERSION=12.3.3
FROM alpine:3.22
ARG TARGETPLATFORM
ARG CONTAINER_APP_VERSION
ARG GRAFANA_VERSION=${CONTAINER_APP_VERSION}
RUN set -e && \
apk --no-cache add dumb-init curl && \
# Detect architecture
if [ -n "$TARGETPLATFORM" ]; then \
echo "TARGETPLATFORM: $TARGETPLATFORM"; \
case "$TARGETPLATFORM" in \
linux/arm64*) ARCH="arm64" ;; \
linux/amd64*) ARCH="amd64" ;; \
*) ARCH="" ;; \
esac; \
else \
echo "TARGETPLATFORM not set, detecting from uname..."; \
UNAME_ARCH=$(uname -m); \
echo "uname -m: $UNAME_ARCH"; \
case "$UNAME_ARCH" in \
aarch64|arm64) ARCH="arm64" ;; \
x86_64) ARCH="amd64" ;; \
*) ARCH="" ;; \
esac; \
fi && \
if [ -z "$ARCH" ]; then \
echo "ERROR: Unsupported architecture"; \
exit 1; \
fi && \
url="https://dl.grafana.com/oss/release/grafana-${GRAFANA_VERSION}.linux-${ARCH}.tar.gz" && \
echo "URL: $url" && \
curl -fSL "$url" | tar -xz -C /tmp && \
mv /tmp/grafana-${GRAFANA_VERSION} /usr/share/grafana && \
apk del curl
# Standard Grafana paths
RUN mkdir -p /etc/grafana /var/lib/grafana /var/log/grafana && \
cp /usr/share/grafana/conf/defaults.ini /etc/grafana/grafana.ini && \
cp /usr/share/grafana/conf/defaults.ini /etc/grafana/defaults.ini
# UID 472 matches official Grafana image for PVC compatibility
RUN adduser -D -u 472 -h /usr/share/grafana grafana && \
chown -R grafana:grafana /usr/share/grafana /etc/grafana /var/lib/grafana /var/log/grafana
USER grafana
WORKDIR /usr/share/grafana
EXPOSE 3000
LABEL org.opencontainers.image.title="Grafana"
LABEL org.opencontainers.image.description="Grafana OSS observability platform"
LABEL org.opencontainers.image.source="https://github.com/grafana/grafana"
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["grafana", "server", \
"--homepath=/usr/share/grafana", \
"--config=/etc/grafana/grafana.ini", \
"cfg:default.paths.data=/var/lib/grafana", \
"cfg:default.paths.logs=/var/log/grafana", \
"cfg:default.paths.plugins=/var/lib/grafana/plugins", \
"cfg:default.paths.provisioning=/etc/grafana/provisioning"]

View file

@ -7,7 +7,7 @@
# so this image only needs: git, Docker CLI, Dagger CLI, ArgoCD CLI, uv, yq, and basic tools.
#
# Usage: Configure runner with label like:
# docker:docker://registry.ops.eblu.me/blumeops/forgejo-runner:latest
# docker:docker://registry.ops.eblu.me/blumeops/runner-job-image:latest
ARG CONTAINER_APP_VERSION=0.19.11

View file

@ -0,0 +1 @@
Rename `containers/forgejo-runner` to `containers/runner-job-image` to distinguish the CI job execution image from the Forgejo runner daemon, fixing a version-check false positive.

View file

@ -67,7 +67,7 @@ Fragments are automatically collected into `CHANGELOG.md` (at repo root) during
The workflow runs on the `k8s` label, which uses the [[forgejo]]-runner in Kubernetes:
- **Runner deployment**: `argocd/manifests/forgejo-runner/`
- **Job image**: `registry.ops.eblu.me/blumeops/forgejo-runner:latest`
- **Job image**: `registry.ops.eblu.me/blumeops/runner-job-image` (commit-SHA tagged)
- **Build engine**: [[dagger]] CLI installed at runtime; Node.js and Python run inside Dagger containers
The job image is built from `containers/forgejo-runner/Dockerfile`.

View file

@ -1,6 +1,5 @@
---
title: Build Grafana Container
status: active
modified: 2026-02-23
tags:
- how-to
@ -23,6 +22,10 @@ Grafana currently uses the upstream `docker.io/grafana/grafana:11.4.0` image via
3. Tag and push to `forge.ops.eblu.me/eblume/grafana:<version>`
4. Add to `mise run container-list` inventory
## Lessons
- **Tarball directory name:** The Grafana OSS tarball extracts to `grafana-<version>` (e.g. `grafana-12.3.3`), *not* `grafana-v<version>`. The `mv` command in the Dockerfile must match this.
## Reference
- Follow [[build-container-image]] for the standard container build workflow

View file

@ -1,6 +1,5 @@
---
title: Kustomize Grafana Deployment
status: active
modified: 2026-02-23
tags:
- how-to

View file

@ -1,6 +1,7 @@
---
title: Upgrade Grafana
status: active
branch: mikado/upgrade-grafana
requires:
- kustomize-grafana-deployment
- build-grafana-container

View file

@ -83,9 +83,9 @@ services:
- name: grafana
type: argocd
last-reviewed: 2026-02-23
current-version: "v11.4.0"
current-version: "12.3.3"
upstream-source: https://github.com/grafana/grafana/releases
notes: Helm chart 8.8.2; Mikado chain to upgrade to 12.x with kustomize
notes: Home-built container; upgrading to 12.x via Mikado chain
- name: cloudnative-pg
type: argocd
@ -190,8 +190,17 @@ services:
current-version: "12.7.0"
upstream-source: https://code.forgejo.org/forgejo/runner/releases
notes: >-
Runner daemon version. Job execution container (containers/forgejo-runner)
tracks Dagger at v0.19.11.
Runner daemon version (code.forgejo.org/forgejo/runner). Job execution
image is tracked separately as runner-job-image.
- name: runner-job-image
type: argocd
last-reviewed: 2026-02-23
current-version: "0.19.11"
upstream-source: https://github.com/dagger/dagger/releases
notes: >-
Forgejo Actions job execution image. CONTAINER_APP_VERSION tracks the
Dagger CLI version, the primary build tool in the image.
- name: nix-container-builder
type: nixos