Compare commits

..

1 Commits

Author SHA1 Message Date
unkin-agent 4256b09c9b arrstack: repoint arrproxy oauth2-proxy image off ghcr
ci/woodpecker/pr/vector-test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline was successful
ghcr.io/oauth2-proxy/oauth2-proxy does not permit anonymous pulls (its
ghcr token endpoint returns DENIED), so artifactapi's anon-only ghcr
remote returns 401 and the sidecar is stuck in ImagePullBackOff. valkey-io
and other ghcr images work because they issue anon tokens.

Repoint the oauth2-proxy sidecar to the docker-internal local registry,
where the multi-arch v7.15.3 image (mirrored from quay.io, identical
digest sha256:10a11657...) is anon-pullable, restoring the arrproxy front
door.
2026-08-16 23:38:44 +10:00
83 changed files with 221 additions and 2593 deletions
@@ -7,7 +7,7 @@ metadata:
annotations:
# Wave 2: serve only after the wave-1 migrate Job completes.
argocd.argoproj.io/sync-wave: "2"
secret.reloader.stakater.com/reload: "arrproxy-pepper,arrproxy-admin-token,arrproxy-db-app,sonarr-apikey,radarr-apikey,prowlarr-apikey"
secret.reloader.stakater.com/reload: "arrproxy-pepper,arrproxy-db-app,sonarr-apikey,radarr-apikey,prowlarr-apikey"
spec:
replicas: 2
selector:
@@ -33,7 +33,7 @@ spec:
type: RuntimeDefault
containers:
- name: api
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/arrproxy-api:v0.3.0
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/arrproxy-api:v0.1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
@@ -59,15 +59,6 @@ spec:
secretKeyRef:
name: arrproxy-pepper
key: pepper
# Machine-mint admin bearer, synced from Vault KV via the
# arrproxy-admin-token VSO. Gates the /api/admin/ route that
# oauth2-proxy intentionally skip-auths so OpenBao on the VMs can
# mint tokens against arrproxy's own bearer.
- name: ARRPROXY_ADMIN_TOKEN
valueFrom:
secretKeyRef:
name: arrproxy-admin-token
key: token
# DSN assembled from the CNPG-generated arrproxy-db-app Secret;
# $(VAR) expansion resolves the two env entries defined above it.
- name: ARRPROXY_DB_USER
@@ -33,20 +33,10 @@ data:
# set-xauthrequest is intentionally NOT set -- it only populates auth_request
# *response* headers, which never reach an --upstreams-proxied backend.
OAUTH2_PROXY_PASS_USER_HEADERS: "true"
# Bypass auth for the *arr proxy API (/<app>/api...) and the machine-mint
# admin route (/api/admin/...). The first matches /sonarr/api; the second
# matches /api/admin/ only -- both routed to the arrproxy-api upstream by the
# catch-all /api/ prefix above. /api/admin/ is protected by arrproxy's OWN
# ARRPROXY_ADMIN_TOKEN bearer (OpenBao on the VMs reaches it via the ingress),
# so it is intentionally oauth-skipped. /api/tokens and /api/me are NOT
# matched and stay oauth-authenticated.
OAUTH2_PROXY_SKIP_AUTH_REGEX: "^/[^/]+/api,^/api/admin/"
# Bypass auth ONLY for the *arr proxy API (/<app>/api...): this matches
# /sonarr/api but NOT /api/tokens or /api/me (which stay authenticated).
OAUTH2_PROXY_SKIP_AUTH_REGEX: "^/[^/]+/api"
OAUTH2_PROXY_EMAIL_DOMAINS: "*"
# Authentik hardcodes email_verified=false in the id_token; without this
# oauth2-proxy rejects the session ("email ... isn't verified") -> 500 on
# /oauth2/callback. Authorization is enforced downstream via ak_groups, so
# accepting the unverified email here is safe.
OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL: "true"
OAUTH2_PROXY_COOKIE_SECURE: "true"
OAUTH2_PROXY_COOKIE_DOMAINS: "arrstack.unkin.net"
OAUTH2_PROXY_WHITELIST_DOMAINS: "arrstack.unkin.net"
@@ -62,7 +62,7 @@ spec:
memory: 64Mi
containers:
- name: oauth2-proxy
image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.3
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/oauth2-proxy:v7.15.3
imagePullPolicy: IfNotPresent
ports:
- containerPort: 4180
@@ -31,7 +31,7 @@ spec:
type: RuntimeDefault
containers:
- name: ui
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/arrproxy-ui:v0.3.0
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/arrproxy-ui:v0.1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
@@ -25,33 +25,6 @@ spec:
type: kv-v2
vaultAuthRef: default
---
# Machine-mint admin bearer token. Seeded (openssl rand) at
# kv/kubernetes/namespace/arrstack/default/arrproxy-admin-token (key: token) and
# shared as the source of truth with the future Vault engine. The default k8s
# role's templated policy already grants read on
# kv/data/kubernetes/namespace/{{sa_namespace}}/{{sa_name}}/* for the
# arrstack/default ServiceAccount, so no terraform-vault change is needed. VSO
# syncs it into the arrproxy-admin-token Secret consumed by arrproxy-api as
# ARRPROXY_ADMIN_TOKEN to gate the bearer-protected /api/admin/ route.
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: arrproxy-admin-token
namespace: arrstack
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
destination:
create: true
name: arrproxy-admin-token
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/arrstack/default/arrproxy-admin-token
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
---
# Authentik OIDC client for the arrstack front door (client_id, client_secret,
# cookie_secret), created by terraform-authentik at
# kv/kubernetes/namespace/arrstack/default/oauth-credentials. VSO syncs it into
-42
View File
@@ -1,42 +0,0 @@
---
# Shared Ceph RGW (S3) bucket for arrstack application backups (the periodic
# radarr/sonarr/prowlarr config+database zip backups), provisioned by the
# in-estate cephrgw-operator. Backups otherwise land on each replica's local
# /config (an emptyDir) and are lost when that pod is rescheduled; routing them
# to S3 lets any replica write and restore them.
#
# Backups are kept in their own bucket (not the arrstack-media one) because they
# carry secrets (Config.xml holds API keys, the DB has all app state) and thus
# warrant separate credentials, lifecycle and retention from the public-ish
# poster art. The three apps share this one bucket, isolated by a per-app key
# prefix (radarr/, sonarr/, prowlarr/) set via <App>__BackupS3__Prefix.
#
# The operator mints the S3 credential Secret (arrstack-backups-s3) in this
# namespace with keys AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and S3_ENDPOINT,
# so no Vault KV seeding is required.
apiVersion: ceph.unkin.net/v1alpha1
kind: ObjectStoreUser
metadata:
name: arrstack-backups
namespace: arrstack
spec:
displayName: "arrstack backups owner"
uid: arrstack-backups
maxBuckets: 5
secretName: arrstack-backups-s3
retainOnDelete: true
---
apiVersion: ceph.unkin.net/v1alpha1
kind: Bucket
metadata:
name: arrstack-backups
namespace: arrstack
spec:
placementTarget: ec
bucketName: arrstack-backups
ownerRef: arrstack-backups
versioning: false
tags:
app: arrstack
purpose: backups
retainOnDelete: true
-7
View File
@@ -7,17 +7,10 @@ resources:
- vaultauth.yaml
- pv-media-tv.yaml
- pv-media-movies.yaml
- pv-mediafs.yaml
- pvc-media-tv.yaml
- pvc-media-movies.yaml
- pvc-mediafs.yaml
- media-bucket.yaml
- backups-bucket.yaml
- postgres
- valkey
- sonarr
- radarr
- prowlarr
- nzbget
- arrproxy
- mediamover
-34
View File
@@ -1,34 +0,0 @@
---
# Shared Ceph RGW (S3) bucket for arrstack media assets (Servarr MediaCover
# posters/fanart and, later, application backups), provisioned by the in-estate
# cephrgw-operator. Moving these off each replica's local /config (an emptyDir)
# lets any radarr/sonarr/prowlarr replica serve covers and store backups without
# leader-local state. The operator mints the S3 credential Secret
# (arrstack-media-s3) in this namespace with keys AWS_ACCESS_KEY_ID,
# AWS_SECRET_ACCESS_KEY and S3_ENDPOINT, so no Vault KV seeding is required.
apiVersion: ceph.unkin.net/v1alpha1
kind: ObjectStoreUser
metadata:
name: arrstack-media
namespace: arrstack
spec:
displayName: "arrstack media assets owner"
uid: arrstack-media
maxBuckets: 5
secretName: arrstack-media-s3
retainOnDelete: true
---
apiVersion: ceph.unkin.net/v1alpha1
kind: Bucket
metadata:
name: arrstack-media
namespace: arrstack
spec:
placementTarget: ec
bucketName: arrstack-media
ownerRef: arrstack-media
versioning: false
tags:
app: arrstack
purpose: media-assets
retainOnDelete: true
@@ -1,95 +0,0 @@
---
# mediamover server: REST API + UI on :8080. Spawns one worker Job per queued
# file using this same image with `worker` args (Job spec lives in code; workers
# run as the default ServiceAccount and mount the same PVCs). The queue is
# in-memory, so keep a single replica; a restart just loses queued entries.
apiVersion: apps/v1
kind: Deployment
metadata:
name: mediamover
namespace: arrstack
spec:
replicas: 1
selector:
matchLabels:
app: mediamover
strategy:
type: Recreate
template:
metadata:
labels:
app: mediamover
spec:
serviceAccountName: mediamover
automountServiceAccountToken: true
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: server
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/mediamover:v0.1.0
imagePullPolicy: IfNotPresent
args:
- server
- --src-root=/srv/src
- --src-pvc=mediafs
- --dst-roots=movies=/srv/dst/movies,tv=/srv/dst/tv
- --dst-pvc=movies=media-movies,tv=media-tv
- --namespace=arrstack
- --image=artifactapi.k8s.syd1.au.unkin.net/docker-internal/mediamover:v0.1.0
ports:
- containerPort: 8080
name: http
protocol: TCP
volumeMounts:
# RW: move mode deletes the source file after a successful copy.
- name: src
mountPath: /srv/src
- name: dst-movies
mountPath: /srv/dst/movies
- name: dst-tv
mountPath: /srv/dst/tv
livenessProbe:
httpGet:
path: /api/limit
port: http
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /api/limit
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
volumes:
- name: src
persistentVolumeClaim:
claimName: mediafs
- name: dst-movies
persistentVolumeClaim:
claimName: media-movies
- name: dst-tv
persistentVolumeClaim:
claimName: media-tv
restartPolicy: Always
@@ -1,40 +0,0 @@
---
# Internal front for mediamover. The existing arrstack (arrproxy) Gateway is
# external and hostname-locked to arrstack.unkin.net, so this tool gets its own
# internal Gateway following the cluster convention (cf. pdbmux).
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
labels:
traefik.io/instance: internal
annotations:
cert-manager.io/cluster-issuer: vault-issuer
cert-manager.io/common-name: mediamover.k8s.syd1.au.unkin.net
cert-manager.io/private-key-size: "4096"
external-dns.alpha.kubernetes.io/hostname: mediamover.k8s.syd1.au.unkin.net
external-dns.alpha.kubernetes.io/target: 198.18.200.4
name: mediamover
namespace: arrstack
spec:
gatewayClassName: traefik-internal
listeners:
- allowedRoutes:
namespaces:
from: Same
hostname: mediamover.k8s.syd1.au.unkin.net
name: http
port: 80
protocol: HTTP
- allowedRoutes:
namespaces:
from: Same
hostname: mediamover.k8s.syd1.au.unkin.net
name: https
port: 443
protocol: HTTPS
tls:
certificateRefs:
- group: ""
kind: Secret
name: mediamover-tls
mode: Terminate
@@ -1,49 +0,0 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: mediamover-http-redirect
namespace: arrstack
spec:
hostnames:
- mediamover.k8s.syd1.au.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: mediamover
sectionName: http
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
matches:
- path:
type: PathPrefix
value: /
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: mediamover
namespace: arrstack
spec:
hostnames:
- mediamover.k8s.syd1.au.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: mediamover
sectionName: https
rules:
- backendRefs:
- group: ""
kind: Service
name: mediamover
port: 8080
weight: 1
matches:
- path:
type: PathPrefix
value: /
@@ -1,11 +0,0 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- serviceaccount.yaml
- rbac.yaml
- deployment.yaml
- service.yaml
- gateway.yaml
- httproute.yaml
-48
View File
@@ -1,48 +0,0 @@
---
# The server creates one worker Job per queued file and polls Job/Pod state to
# track progress and clean up.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: mediamover
namespace: arrstack
rules:
- apiGroups:
- batch
resources:
- jobs
verbs:
- create
- get
- list
- watch
- delete
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- pods/log
verbs:
- get
- list
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: mediamover
namespace: arrstack
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: mediamover
subjects:
- kind: ServiceAccount
name: mediamover
namespace: arrstack
@@ -1,19 +0,0 @@
---
# Must stay named "mediamover" on port 8080: worker Jobs default their progress
# callback URL to http://mediamover.<namespace>.svc:8080.
apiVersion: v1
kind: Service
metadata:
name: mediamover
namespace: arrstack
spec:
internalTrafficPolicy: Cluster
ports:
- name: http
port: 8080
protocol: TCP
targetPort: http
selector:
app: mediamover
sessionAffinity: None
type: ClusterIP
@@ -1,6 +0,0 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: mediamover
namespace: arrstack
@@ -1,45 +0,0 @@
---
# Ceph RGW (S3) backup target for the shared arrstack CNPG cluster, provisioned
# by the in-estate cephrgw-operator: one dedicated bucket + owner user. CNPG
# reads the S3 credential Secret (cnpg-arrstack-backup-s3) from this namespace.
apiVersion: ceph.unkin.net/v1alpha1
kind: ObjectStoreUser
metadata:
name: cnpg-arrstack-backup
namespace: arrstack
spec:
displayName: "CNPG backup owner (arrstack)"
uid: cnpg-arrstack-backup
maxBuckets: 5
secretName: cnpg-arrstack-backup-s3
retainOnDelete: true
---
apiVersion: ceph.unkin.net/v1alpha1
kind: Bucket
metadata:
name: cnpg-arrstack
namespace: arrstack
spec:
placementTarget: ec
bucketName: cnpg-arrstack
ownerRef: cnpg-arrstack-backup
versioning: false
tags:
app: arrstack
purpose: cnpg-backup
retainOnDelete: true
---
# Nightly base backup on top of always-on WAL archiving. Staggered from the
# other CNPG clusters (6-field cron, seconds first).
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: cnpg-arrstack-nightly
namespace: arrstack
spec:
schedule: "0 45 3 * * *"
immediate: false
backupOwnerReference: self
method: barmanObjectStore
cluster:
name: arrstack-postgres
@@ -1,161 +0,0 @@
---
# Shared PostgreSQL backend for the -unkin2 fork sonarr/radarr/prowlarr, whose
# Npgsql/EF Core provider moves each *arr off SQLite into Postgres and makes the
# shared-nothing, active-active multi-replica deployment possible. One cluster,
# one throwaway initdb owner ("app"), and three managed login roles — one per
# app — each with its own per-app database (see database-*.yaml). Role passwords
# come from the VSO-synced <app>-db Secrets (vaultstaticsecret.yaml), so no
# credential is rendered into git.
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: arrstack-postgres
namespace: arrstack
spec:
inheritedMetadata:
annotations:
k8up.io/backup: "false"
affinity:
podAntiAffinityType: preferred
backup:
retentionPolicy: 30d
barmanObjectStore:
destinationPath: s3://cnpg-arrstack
endpointURL: https://s3.ceph.unkin.net
endpointCA:
name: vault-ca-cert
key: ca.crt
s3Credentials:
accessKeyId:
name: cnpg-arrstack-backup-s3
key: AWS_ACCESS_KEY_ID
secretAccessKey:
name: cnpg-arrstack-backup-s3
key: AWS_SECRET_ACCESS_KEY
serverName: arrstack
data:
compression: bzip2
jobs: 2
wal:
compression: zstd
maxParallel: 2
bootstrap:
initdb:
# Throwaway owner + database: the real per-app databases are provisioned by
# the CNPG Database CRDs, owned by the managed roles below.
database: app
encoding: UTF8
localeCType: C
localeCollate: C
owner: app
managed:
roles:
- name: sonarr
ensure: present
comment: Sonarr application role (owns sonarr-main)
login: true
superuser: false
createdb: false
createrole: false
inherit: true
replication: false
connectionLimit: -1
passwordSecret:
name: sonarr-db
- name: radarr
ensure: present
comment: Radarr application role (owns radarr-main)
login: true
superuser: false
createdb: false
createrole: false
inherit: true
replication: false
connectionLimit: -1
passwordSecret:
name: radarr-db
- name: prowlarr
ensure: present
comment: Prowlarr application role (owns prowlarr-main)
login: true
superuser: false
createdb: false
createrole: false
inherit: true
replication: false
connectionLimit: -1
passwordSecret:
name: prowlarr-db
enablePDB: true
enableSuperuserAccess: false
failoverDelay: 0
imageName: ghcr.io/cloudnative-pg/postgresql:17-system-trixie
instances: 3
logLevel: info
maxSyncReplicas: 0
minSyncReplicas: 0
monitoring:
customQueriesConfigMap:
- key: queries
name: cnpg-default-monitoring
disableDefaultQueries: false
enablePodMonitor: false
postgresql:
parameters:
archive_mode: "on"
archive_timeout: 5min
dynamic_shared_memory_type: posix
effective_cache_size: 256MB
full_page_writes: "on"
log_destination: csvlog
log_directory: /controller/log
log_filename: postgres
log_rotation_age: "0"
log_rotation_size: "0"
log_truncate_on_rotation: "false"
logging_collector: "on"
max_connections: "200"
max_parallel_workers: "16"
max_replication_slots: "16"
max_worker_processes: "16"
shared_buffers: 128MB
shared_memory_type: mmap
ssl_max_protocol_version: TLSv1.3
ssl_min_protocol_version: TLSv1.3
wal_keep_size: 256MB
wal_level: logical
wal_log_hints: "on"
wal_receiver_timeout: 5s
wal_sender_timeout: 5s
syncReplicaElectionConstraint:
enabled: false
primaryUpdateMethod: restart
primaryUpdateStrategy: unsupervised
probes:
liveness:
isolationCheck:
connectionTimeout: 1000
enabled: true
requestTimeout: 1000
replicationSlots:
highAvailability:
enabled: true
slotPrefix: _cnpg_
synchronizeReplicas:
enabled: true
updateInterval: 30
resources:
limits:
cpu: "1"
memory: 2Gi
requests:
cpu: 250m
memory: 1Gi
smartShutdownTimeout: 180
startDelay: 3600
stopDelay: 1800
storage:
resizeInUseVolumes: true
size: 10Gi
storageClass: cephrbd-fast-delete
switchoverDelay: 3600
@@ -1,15 +0,0 @@
---
# Per-app database owned by the prowlarr managed role. The fork's provider runs
# its own schema migrations on first start (advisory-locked, so only one replica
# migrates). retain: the database survives a Database CRD delete.
apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
name: prowlarr-main
namespace: arrstack
spec:
cluster:
name: arrstack-postgres
name: prowlarr-main
owner: prowlarr
databaseReclaimPolicy: retain
@@ -1,15 +0,0 @@
---
# Per-app database owned by the radarr managed role. The fork's provider runs its
# own schema migrations on first start (advisory-locked, so only one replica
# migrates). retain: the database survives a Database CRD delete.
apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
name: radarr-main
namespace: arrstack
spec:
cluster:
name: arrstack-postgres
name: radarr-main
owner: radarr
databaseReclaimPolicy: retain
@@ -1,15 +0,0 @@
---
# Per-app database owned by the sonarr managed role. The fork's provider runs its
# own schema migrations on first start (advisory-locked, so only one replica
# migrates). retain: the database survives a Database CRD delete.
apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
name: sonarr-main
namespace: arrstack
spec:
cluster:
name: arrstack-postgres
name: sonarr-main
owner: sonarr
databaseReclaimPolicy: retain
@@ -1,11 +0,0 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- vaultstaticsecret.yaml
- cnpg_cluster.yaml
- cnpg_backup.yaml
- database-sonarr.yaml
- database-radarr.yaml
- database-prowlarr.yaml
@@ -1,60 +0,0 @@
---
# Per-app Postgres role credentials. Each is seeded out-of-band at
# kv/kubernetes/namespace/arrstack/default/<app>-db (keys: username, password);
# the default k8s role's templated policy already grants read on
# kv/data/kubernetes/namespace/{{sa_namespace}}/{{sa_name}}/* for the
# arrstack/default ServiceAccount, so no terraform-vault change is needed. VSO
# syncs each into the <app>-db Secret, which is both the CNPG managed role's
# passwordSecret (cnpg_cluster.yaml) and the source of the app Deployment's
# <App>__Postgres__User/__Password env. Wave 0: must exist before the Cluster
# (wave 1) reconciles the roles.
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: sonarr-db
namespace: arrstack
spec:
destination:
create: true
name: sonarr-db
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/arrstack/default/sonarr-db
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: radarr-db
namespace: arrstack
spec:
destination:
create: true
name: radarr-db
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/arrstack/default/radarr-db
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: prowlarr-db
namespace: arrstack
spec:
destination:
create: true
name: prowlarr-db
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/arrstack/default/prowlarr-db
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
@@ -1,33 +0,0 @@
---
# Non-secret env for the -unkin2 fork. The fork reads Servarr config from
# Prowlarr__<Section>__<Key> env (no config.xml edits, no s6/PUID). Postgres
# wiring points every replica at the same shared DB (arrstack-postgres-rw /
# prowlarr-main); Auth__Method=External defers UI auth to arrproxy/oauth2-proxy;
# Server__UrlBase keeps the /prowlarr prefix so arrproxy path-routing works;
# App__InstanceName is identical across replicas (shared session-cookie name).
# User/Password/ApiKey come from Secrets (see deployment.yaml), not here.
apiVersion: v1
kind: ConfigMap
metadata:
name: prowlarr-env
namespace: arrstack
data:
Prowlarr__Postgres__Host: arrstack-postgres-rw.arrstack.svc.cluster.local
Prowlarr__Postgres__Port: "5432"
Prowlarr__Postgres__MainDb: prowlarr-main
Prowlarr__Log__DbEnabled: "false"
Prowlarr__Auth__Method: External
Prowlarr__Auth__Required: DisabledForLocalAddresses
Prowlarr__App__InstanceName: Prowlarr
Prowlarr__Server__Port: "9696"
Prowlarr__Server__UrlBase: /prowlarr
Prowlarr__Update__Mechanism: External
# Shared arrstack Valkey (valkey-operator). Setting Host is what activates the
# fork's #14 Redis features (SignalR backplane, cross-replica cache-invalidation
# bus, distributed rate limiter): RedisOptions.IsConfigured gates purely on a
# non-empty Host, so there is no separate Enabled flag. The operator leaves the
# default user passwordless (jellyfin parity), so no Password/Ssl is wired.
# Channels/keys are namespaced by this fork's prowlarr:ratelimit: prefix, so the
# one cluster is safe to share with sonarr/radarr.
Prowlarr__Redis__Host: valkey-arrstack-valkey.arrstack.svc.cluster.local
Prowlarr__Redis__Port: "6379"
+64 -123
View File
@@ -4,21 +4,11 @@ kind: Deployment
metadata:
name: prowlarr
namespace: arrstack
annotations:
# prowlarr-env is a plain (unhashed) ConfigMap consumed by fixed-name envFrom,
# so editing it does not roll the Deployment on its own. Reloader watches the
# referenced ConfigMap and triggers a rolling restart on change, so adding the
# Redis env activates the #14 features on the next ArgoCD sync without a manual
# `rollout restart`.
configmap.reloader.stakater.com/auto: "true"
spec:
# Active-active: the -unkin2 fork keeps all state in the shared Postgres
# (arrstack-postgres) and coordinates via Postgres advisory locks, so N
# replicas run concurrently behind the prowlarr Service. RollingUpdate is safe
# — no SQLite, no RWO lock.
replicas: 3
replicas: 1
strategy:
type: RollingUpdate
# RWO config PVC + single stateful SQLite DB: never run two pods at once.
type: Recreate
selector:
matchLabels:
app: prowlarr
@@ -28,114 +18,74 @@ spec:
app: prowlarr
spec:
securityContext:
# Fork image has no USER (runs as root by default); pin it to a non-root
# UID and group-write the shared RWX CephFS /config. OnRootMismatch
# avoids a recursive chown of the whole volume.
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
initContainers:
# Gate the app on its own Postgres database+role being reachable, instead
# of relying on ArgoCD sync-waves (which deadlock if apps aren't Healthy).
# waitfordb reads the PG* env as a libpq fallback, so the password never lands in argv.
- name: wait-for-db
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/waitfordb:v0.1.0
env:
- name: WAITFORDB_TIMEOUT
value: 5m
- name: WAITFORDB_SSLMODE
value: disable
- name: PGHOST
value: arrstack-postgres-rw.arrstack.svc.cluster.local
- name: PGPORT
value: "5432"
- name: PGDATABASE
value: prowlarr-main
- name: PGUSER
valueFrom:
secretKeyRef:
name: prowlarr-db
key: username
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: prowlarr-db
key: password
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
containers:
- name: prowlarr
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/prowlarr:v2.6.2-unkin6
# Enforce the Vault-sourced API key in /config/config.xml before the app
# starts. Vault is source of truth (override bootstrap): the key is minted
# in Vault, synced by VSO into the prowlarr-apikey Secret, and written here.
# Runs as root to fix ownership; touches only the <ApiKey> element.
- name: apikey-init
image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/library/busybox:1.37.0
imagePullPolicy: IfNotPresent
command:
- /app/Prowlarr
args:
- -nobrowser
- -data=/config
# Required: bypass the single-instance guard so multiple replicas
# can share one /config. Cross-replica safety is the Postgres layer,
# not a local lock file.
- -nosingleinstancecheck
ports:
- name: http
containerPort: 9696
protocol: TCP
envFrom:
- configMapRef:
name: prowlarr-env
securityContext:
runAsUser: 0
env:
- name: Prowlarr__Postgres__User
valueFrom:
secretKeyRef:
name: prowlarr-db
key: username
- name: Prowlarr__Postgres__Password
valueFrom:
secretKeyRef:
name: prowlarr-db
key: password
- name: Prowlarr__Auth__ApiKey
- name: API_KEY
valueFrom:
secretKeyRef:
name: prowlarr-apikey
key: apitoken
# Backup object store (shared arrstack-backups Ceph RGW bucket,
# per-app key prefix). Routes the periodic config+DB zip backups off
# the ephemeral /config so any replica can write and restore them.
# Consumed by the -unkin3+ image; older images ignore these unknown
# config keys. Creds Secret minted by cephrgw-operator.
- name: Prowlarr__BackupS3__Endpoint
valueFrom:
secretKeyRef:
name: arrstack-backups-s3
key: S3_ENDPOINT
- name: Prowlarr__BackupS3__AccessKey
valueFrom:
secretKeyRef:
name: arrstack-backups-s3
key: AWS_ACCESS_KEY_ID
- name: Prowlarr__BackupS3__SecretKey
valueFrom:
secretKeyRef:
name: arrstack-backups-s3
key: AWS_SECRET_ACCESS_KEY
- name: Prowlarr__BackupS3__Bucket
value: arrstack-backups
- name: Prowlarr__BackupS3__Prefix
value: prowlarr
- name: Prowlarr__BackupS3__ForcePathStyle
value: "true"
- name: Prowlarr__BackupS3__CaCertPath
value: /etc/ssl/vault-ca/ca.crt
command:
- sh
- -c
- |
set -eu
case "$API_KEY" in
"" | *[!0-9a-fA-F]*)
echo "apikey-init: API_KEY missing or not hex; refusing" >&2
exit 1
;;
esac
CFG=/config/config.xml
if [ ! -f "$CFG" ]; then
printf '<Config>\n <ApiKey>%s</ApiKey>\n</Config>\n' "$API_KEY" > "$CFG"
elif grep -q '<ApiKey>' "$CFG"; then
sed -i "s|<ApiKey>[^<]*</ApiKey>|<ApiKey>${API_KEY}</ApiKey>|" "$CFG"
else
sed -i "s|<Config>|<Config>\n <ApiKey>${API_KEY}</ApiKey>|" "$CFG"
fi
chown 1000:1000 "$CFG"
chmod 600 "$CFG"
echo "apikey-init: <ApiKey> enforced from Vault"
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 200m
memory: 64Mi
volumeMounts:
- name: config
mountPath: /config
containers:
- name: prowlarr
image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/linuxserver/prowlarr:2.5.2
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 9696
protocol: TCP
env:
- name: PUID
value: "1000"
- name: PGID
value: "1000"
- name: TZ
value: Australia/Sydney
livenessProbe:
httpGet:
path: /prowlarr/ping
path: /ping
port: http
initialDelaySeconds: 30
periodSeconds: 30
@@ -143,7 +93,7 @@ spec:
failureThreshold: 3
readinessProbe:
httpGet:
path: /prowlarr/ping
path: /ping
port: http
initialDelaySeconds: 10
periodSeconds: 10
@@ -155,20 +105,11 @@ spec:
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
memory: 512Mi
volumeMounts:
- name: config
mountPath: /config
- name: vault-ca
mountPath: /etc/ssl/vault-ca
readOnly: true
volumes:
- name: config
emptyDir: {}
# Estate CA for validating the Ceph RGW (s3.ceph.unkin.net) TLS cert.
- name: vault-ca
secret:
secretName: vault-ca-cert
items:
- key: ca.crt
path: ca.crt
persistentVolumeClaim:
claimName: prowlarr-config
@@ -3,8 +3,8 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- pvc-config.yaml
- vaultstaticsecret.yaml
- configmap.yaml
- deployment.yaml
- service.yaml
- gateway.yaml
+4 -5
View File
@@ -1,7 +1,6 @@
---
# Prowlarr /config. RWX on CephFS so all replicas share it (the -unkin2 fork
# keeps the database in Postgres; /config now holds only config.xml + assets,
# which tolerate — and want — shared access). Retain: this is state.
# Prowlarr config + SQLite DB. RWO on cephrbd (block) — the arr apps' SQLite
# does not tolerate CephFS locking. Retain: this is state.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
@@ -9,9 +8,9 @@ metadata:
namespace: arrstack
spec:
accessModes:
- ReadWriteMany
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: cephfs-raid5-retain
storageClassName: cephrbd-fast-retain
volumeMode: Filesystem
-31
View File
@@ -1,31 +0,0 @@
---
# Static PV exposing the legacy mediafs CephFS filesystem root for the upcoming
# mediamover tool. clusterID only selects the monitor set from the csi config;
# fsName selects the actual filesystem, so the ssd_ec clusterID still reaches
# mediafs on the same cluster. Staged with the dedicated ceph-mediafs client.
apiVersion: v1
kind: PersistentVolume
metadata:
name: arrstack-mediafs
spec:
capacity:
storage: 10Ti
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
volumeMode: Filesystem
claimRef:
namespace: arrstack
name: mediafs
csi:
driver: cephfs.csi.ceph.com
volumeHandle: arrstack-mediafs-static
nodeStageSecretRef:
name: ceph-mediafs-secret
namespace: csi-cephfs
volumeAttributes:
staticVolume: "true"
clusterID: cephfs_csi_ssd_ec_4_1
fsName: mediafs
rootPath: /
-20
View File
@@ -1,20 +0,0 @@
---
# Legacy mediafs filesystem root, statically bound to the arrstack-mediafs PV
# for the upcoming mediamover tool. storageClassName "" + volumeName disables
# dynamic provisioning and binds the pre-created static PV.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mediafs
namespace: arrstack
annotations:
k8up.io/backup: "false"
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Ti
storageClassName: ""
volumeName: arrstack-mediafs
volumeMode: Filesystem
-33
View File
@@ -1,33 +0,0 @@
---
# Non-secret env for the -unkin2 fork. The fork reads Servarr config from
# Radarr__<Section>__<Key> env (no config.xml edits, no s6/PUID). Postgres wiring
# points every replica at the same shared DB (arrstack-postgres-rw / radarr-main);
# Auth__Method=External defers UI auth to arrproxy/oauth2-proxy; Server__UrlBase
# keeps the /radarr prefix so arrproxy path-routing works; App__InstanceName is
# identical across replicas (shared session-cookie name). User/Password/ApiKey
# come from Secrets (see deployment.yaml), not here.
apiVersion: v1
kind: ConfigMap
metadata:
name: radarr-env
namespace: arrstack
data:
Radarr__Postgres__Host: arrstack-postgres-rw.arrstack.svc.cluster.local
Radarr__Postgres__Port: "5432"
Radarr__Postgres__MainDb: radarr-main
Radarr__Log__DbEnabled: "false"
Radarr__Auth__Method: External
Radarr__Auth__Required: DisabledForLocalAddresses
Radarr__App__InstanceName: Radarr
Radarr__Server__Port: "7878"
Radarr__Server__UrlBase: /radarr
Radarr__Update__Mechanism: External
# Shared arrstack Valkey (valkey-operator). Setting Host is what activates the
# fork's #14 Redis features (SignalR backplane, cross-replica cache-invalidation
# bus, distributed rate limiter): RedisOptions.IsConfigured gates purely on a
# non-empty Host, so there is no separate Enabled flag. The operator leaves the
# default user passwordless (jellyfin parity), so no Password/Ssl is wired.
# Channels/keys are namespaced by this fork's radarr:ratelimit: prefix, so the
# one cluster is safe to share with sonarr/prowlarr.
Radarr__Redis__Host: valkey-arrstack-valkey.arrstack.svc.cluster.local
Radarr__Redis__Port: "6379"
+63 -192
View File
@@ -4,21 +4,11 @@ kind: Deployment
metadata:
name: radarr
namespace: arrstack
annotations:
# radarr-env is a plain (unhashed) ConfigMap consumed by fixed-name envFrom,
# so editing it does not roll the Deployment on its own. Reloader watches the
# referenced ConfigMap and triggers a rolling restart on change, so adding the
# Redis env activates the #14 features on the next ArgoCD sync without a manual
# `rollout restart`.
configmap.reloader.stakater.com/auto: "true"
spec:
# Active-active: the -unkin2 fork keeps all state in the shared Postgres
# (arrstack-postgres) and coordinates via Postgres advisory locks, so N
# replicas run concurrently behind the radarr Service. RollingUpdate is safe —
# no SQLite, no RWO lock.
replicas: 3
replicas: 1
strategy:
type: RollingUpdate
# RWO config PVC + single stateful SQLite DB: never run two pods at once.
type: Recreate
selector:
matchLabels:
app: radarr
@@ -28,139 +18,74 @@ spec:
app: radarr
spec:
securityContext:
# Fork image has no USER (runs as root by default); pin it to a non-root
# UID and group-write the shared RWX CephFS /config (MediaCover etc.).
# OnRootMismatch avoids a recursive chown of the whole media tree.
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
initContainers:
# Gate the app on its own Postgres database+role being reachable, instead
# of relying on ArgoCD sync-waves (which deadlock if apps aren't Healthy).
# waitfordb reads the PG* env as a libpq fallback, so the password never lands in argv.
- name: wait-for-db
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/waitfordb:v0.1.0
env:
- name: WAITFORDB_TIMEOUT
value: 5m
- name: WAITFORDB_SSLMODE
value: disable
- name: PGHOST
value: arrstack-postgres-rw.arrstack.svc.cluster.local
- name: PGPORT
value: "5432"
- name: PGDATABASE
value: radarr-main
- name: PGUSER
valueFrom:
secretKeyRef:
name: radarr-db
key: username
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: radarr-db
key: password
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
containers:
- name: radarr
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/radarr:v6.4.2-unkin7
# Enforce the Vault-sourced API key in /config/config.xml before the app
# starts. Vault is source of truth (override bootstrap): the key is minted
# in Vault, synced by VSO into the radarr-apikey Secret, and written here.
# Runs as root to fix ownership; touches only the <ApiKey> element.
- name: apikey-init
image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/library/busybox:1.37.0
imagePullPolicy: IfNotPresent
command:
- /app/Radarr
args:
- -nobrowser
- -data=/config
# Required: bypass the single-instance guard so multiple replicas
# can share one /config. Cross-replica safety is the Postgres layer,
# not a local lock file.
- -nosingleinstancecheck
ports:
- name: http
containerPort: 7878
protocol: TCP
envFrom:
- configMapRef:
name: radarr-env
securityContext:
runAsUser: 0
env:
- name: Radarr__Postgres__User
valueFrom:
secretKeyRef:
name: radarr-db
key: username
- name: Radarr__Postgres__Password
valueFrom:
secretKeyRef:
name: radarr-db
key: password
- name: Radarr__Auth__ApiKey
- name: API_KEY
valueFrom:
secretKeyRef:
name: radarr-apikey
key: apitoken
# MediaCover object store (shared Ceph RGW bucket). Serves posters/fanart
# from S3 so any replica can render them instead of the leader-local
# emptyDir /config. Consumed by the -unkin3+ image; older images ignore
# these unknown config keys. Creds Secret is minted by cephrgw-operator.
- name: Radarr__MediaCover__S3__Endpoint
valueFrom:
secretKeyRef:
name: arrstack-media-s3
key: S3_ENDPOINT
- name: Radarr__MediaCover__S3__AccessKey
valueFrom:
secretKeyRef:
name: arrstack-media-s3
key: AWS_ACCESS_KEY_ID
- name: Radarr__MediaCover__S3__SecretKey
valueFrom:
secretKeyRef:
name: arrstack-media-s3
key: AWS_SECRET_ACCESS_KEY
- name: Radarr__MediaCover__S3__Bucket
value: arrstack-media
- name: Radarr__MediaCover__S3__Prefix
value: radarr
- name: Radarr__MediaCover__S3__ForcePathStyle
value: "true"
- name: Radarr__MediaCover__S3__CaCertPath
value: /etc/ssl/vault-ca/ca.crt
# Backup object store (shared arrstack-backups Ceph RGW bucket,
# per-app key prefix). Routes the periodic config+DB zip backups off
# the ephemeral /config so any replica can write and restore them.
- name: Radarr__BackupS3__Endpoint
valueFrom:
secretKeyRef:
name: arrstack-backups-s3
key: S3_ENDPOINT
- name: Radarr__BackupS3__AccessKey
valueFrom:
secretKeyRef:
name: arrstack-backups-s3
key: AWS_ACCESS_KEY_ID
- name: Radarr__BackupS3__SecretKey
valueFrom:
secretKeyRef:
name: arrstack-backups-s3
key: AWS_SECRET_ACCESS_KEY
- name: Radarr__BackupS3__Bucket
value: arrstack-backups
- name: Radarr__BackupS3__Prefix
value: radarr
- name: Radarr__BackupS3__ForcePathStyle
value: "true"
- name: Radarr__BackupS3__CaCertPath
value: /etc/ssl/vault-ca/ca.crt
command:
- sh
- -c
- |
set -eu
case "$API_KEY" in
"" | *[!0-9a-fA-F]*)
echo "apikey-init: API_KEY missing or not hex; refusing" >&2
exit 1
;;
esac
CFG=/config/config.xml
if [ ! -f "$CFG" ]; then
printf '<Config>\n <ApiKey>%s</ApiKey>\n</Config>\n' "$API_KEY" > "$CFG"
elif grep -q '<ApiKey>' "$CFG"; then
sed -i "s|<ApiKey>[^<]*</ApiKey>|<ApiKey>${API_KEY}</ApiKey>|" "$CFG"
else
sed -i "s|<Config>|<Config>\n <ApiKey>${API_KEY}</ApiKey>|" "$CFG"
fi
chown 1000:1000 "$CFG"
chmod 600 "$CFG"
echo "apikey-init: <ApiKey> enforced from Vault"
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 200m
memory: 64Mi
volumeMounts:
- name: config
mountPath: /config
containers:
- name: radarr
image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/linuxserver/radarr:6.3.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 7878
protocol: TCP
env:
- name: PUID
value: "1000"
- name: PGID
value: "1000"
- name: TZ
value: Australia/Sydney
livenessProbe:
httpGet:
path: /radarr/ping
path: /ping
port: http
initialDelaySeconds: 30
periodSeconds: 30
@@ -168,7 +93,7 @@ spec:
failureThreshold: 3
readinessProbe:
httpGet:
path: /radarr/ping
path: /ping
port: http
initialDelaySeconds: 10
periodSeconds: 10
@@ -186,64 +111,10 @@ spec:
mountPath: /config
- name: media-movies
mountPath: /media/movies
- name: vault-ca
mountPath: /etc/ssl/vault-ca
readOnly: true
# exportarr sidecar: polls the local replica's API and exposes Prometheus
# metrics on :9708 (scraped by the radarr-exportarr VMPodScrape).
- name: exportarr
image: artifactapi.k8s.syd1.au.unkin.net/ghcr/onedr0p/exportarr:v2.3.0
imagePullPolicy: IfNotPresent
args:
- radarr
env:
- name: PORT
value: "9708"
# URL includes the /radarr UrlBase (Radarr__Server__UrlBase).
- name: URL
value: http://localhost:7878/radarr
- name: APIKEY
valueFrom:
secretKeyRef:
name: radarr-apikey
key: apitoken
ports:
- name: metrics
containerPort: 9708
protocol: TCP
livenessProbe:
httpGet:
path: /healthz
port: metrics
initialDelaySeconds: 15
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz
port: metrics
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 100m
memory: 128Mi
volumes:
- name: config
emptyDir: {}
persistentVolumeClaim:
claimName: radarr-config
- name: media-movies
persistentVolumeClaim:
claimName: media-movies
# Estate CA for validating the Ceph RGW (s3.ceph.unkin.net) TLS cert.
- name: vault-ca
secret:
secretName: vault-ca-cert
items:
- key: ca.crt
path: ca.crt
+1 -2
View File
@@ -3,10 +3,9 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- pvc-config.yaml
- vaultstaticsecret.yaml
- configmap.yaml
- deployment.yaml
- service.yaml
- gateway.yaml
- httproute.yaml
- vmpodscrape.yaml
+4 -5
View File
@@ -1,7 +1,6 @@
---
# Radarr /config. RWX on CephFS so all replicas share it (the -unkin2 fork keeps
# the database in Postgres; /config now holds only config.xml + MediaCover, which
# tolerate — and want — shared access). Retain: this is state.
# Radarr config + SQLite DB. RWO on cephrbd (block) — the arr apps' SQLite
# does not tolerate CephFS locking. Retain: this is state.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
@@ -9,9 +8,9 @@ metadata:
namespace: arrstack
spec:
accessModes:
- ReadWriteMany
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: cephfs-raid5-retain
storageClassName: cephrbd-fast-retain
volumeMode: Filesystem
@@ -1,16 +0,0 @@
---
# Scrape the exportarr sidecar (:9708) on every radarr pod. Picked up by the
# observability VMAgent (selectAllByDefault). Pod-level rather than
# VMServiceScrape because the radarr Service doesn't expose the metrics port.
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMPodScrape
metadata:
name: radarr-exportarr
namespace: arrstack
spec:
selector:
matchLabels:
app: radarr
podMetricsEndpoints:
- port: metrics
path: /metrics
-33
View File
@@ -1,33 +0,0 @@
---
# Non-secret env for the -unkin2 fork. The fork reads Servarr config from
# Sonarr__<Section>__<Key> env (no config.xml edits, no s6/PUID). Postgres wiring
# points every replica at the same shared DB (arrstack-postgres-rw / sonarr-main);
# Auth__Method=External defers UI auth to arrproxy/oauth2-proxy; Server__UrlBase
# keeps the /sonarr prefix so arrproxy path-routing works; App__InstanceName is
# identical across replicas (shared session-cookie name). User/Password/ApiKey
# come from Secrets (see deployment.yaml), not here.
apiVersion: v1
kind: ConfigMap
metadata:
name: sonarr-env
namespace: arrstack
data:
Sonarr__Postgres__Host: arrstack-postgres-rw.arrstack.svc.cluster.local
Sonarr__Postgres__Port: "5432"
Sonarr__Postgres__MainDb: sonarr-main
Sonarr__Log__DbEnabled: "false"
Sonarr__Auth__Method: External
Sonarr__Auth__Required: DisabledForLocalAddresses
Sonarr__App__InstanceName: Sonarr
Sonarr__Server__Port: "8989"
Sonarr__Server__UrlBase: /sonarr
Sonarr__Update__Mechanism: External
# Shared arrstack Valkey (valkey-operator). Setting Host is what activates the
# fork's #14 Redis features (SignalR backplane, cross-replica cache-invalidation
# bus, distributed rate limiter): RedisOptions.IsConfigured gates purely on a
# non-empty Host, so there is no separate Enabled flag. The operator leaves the
# default user passwordless (jellyfin parity), so no Password/Ssl is wired.
# Channels/keys are namespaced by this fork's sonarr:ratelimit: prefix, so the
# one cluster is safe to share with radarr/prowlarr.
Sonarr__Redis__Host: valkey-arrstack-valkey.arrstack.svc.cluster.local
Sonarr__Redis__Port: "6379"
+65 -193
View File
@@ -4,21 +4,11 @@ kind: Deployment
metadata:
name: sonarr
namespace: arrstack
annotations:
# sonarr-env is a plain (unhashed) ConfigMap consumed by fixed-name envFrom,
# so editing it does not roll the Deployment on its own. Reloader watches the
# referenced ConfigMap and triggers a rolling restart on change, so adding the
# Redis env activates the #14 features on the next ArgoCD sync without a manual
# `rollout restart`.
configmap.reloader.stakater.com/auto: "true"
spec:
# Active-active: the -unkin2 fork keeps all state in the shared Postgres
# (arrstack-postgres) and coordinates via Postgres advisory locks, so N
# replicas run concurrently behind the sonarr Service. RollingUpdate is safe —
# no SQLite, no RWO lock.
replicas: 3
replicas: 1
strategy:
type: RollingUpdate
# RWO config PVC + single stateful SQLite DB: never run two pods at once.
type: Recreate
selector:
matchLabels:
app: sonarr
@@ -28,141 +18,77 @@ spec:
app: sonarr
spec:
securityContext:
# Fork image has no USER (runs as root by default); pin it to a non-root
# UID and group-write the shared RWX CephFS /config (MediaCover etc.).
# LinuxServer images init as root via s6 then step down to PUID/PGID.
# fsGroup makes the shared CephFS group-writable for that user;
# OnRootMismatch avoids a recursive chown of the whole media tree.
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
initContainers:
# Gate the app on its own Postgres database+role being reachable, instead
# of relying on ArgoCD sync-waves (which deadlock if apps aren't Healthy).
# waitfordb reads the PG* env as a libpq fallback, so the password never lands in argv.
- name: wait-for-db
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/waitfordb:v0.1.0
env:
- name: WAITFORDB_TIMEOUT
value: 5m
- name: WAITFORDB_SSLMODE
value: disable
- name: PGHOST
value: arrstack-postgres-rw.arrstack.svc.cluster.local
- name: PGPORT
value: "5432"
- name: PGDATABASE
value: sonarr-main
- name: PGUSER
valueFrom:
secretKeyRef:
name: sonarr-db
key: username
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: sonarr-db
key: password
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
containers:
- name: sonarr
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/sonarr:v5.0.0-unkin6
# Enforce the Vault-sourced API key in /config/config.xml before the app
# starts. Vault is source of truth (override bootstrap): the key is minted
# in Vault, synced by VSO into the sonarr-apikey Secret, and written here.
# Runs as root to fix ownership; touches only the <ApiKey> element.
- name: apikey-init
image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/library/busybox:1.37.0
imagePullPolicy: IfNotPresent
command:
- /app/Sonarr
args:
- -nobrowser
- -data=/config
# Required: bypass the single-instance guard so multiple replicas
# can share one /config. Cross-replica safety is the Postgres layer,
# not a local lock file.
- -nosingleinstancecheck
ports:
- name: http
containerPort: 8989
protocol: TCP
envFrom:
- configMapRef:
name: sonarr-env
securityContext:
runAsUser: 0
env:
- name: Sonarr__Postgres__User
valueFrom:
secretKeyRef:
name: sonarr-db
key: username
- name: Sonarr__Postgres__Password
valueFrom:
secretKeyRef:
name: sonarr-db
key: password
- name: Sonarr__Auth__ApiKey
- name: API_KEY
valueFrom:
secretKeyRef:
name: sonarr-apikey
key: apitoken
# MediaCover object store (shared arrstack-media Ceph RGW bucket,
# partitioned from radarr by the sonarr key prefix). Serves
# posters/fanart from S3 so any replica renders them instead of the
# leader-local emptyDir /config. Consumed by the -unkin3+ image;
# older images ignore these unknown config keys. Creds Secret minted
# by cephrgw-operator.
- name: Sonarr__MediaCoverS3__Endpoint
valueFrom:
secretKeyRef:
name: arrstack-media-s3
key: S3_ENDPOINT
- name: Sonarr__MediaCoverS3__AccessKey
valueFrom:
secretKeyRef:
name: arrstack-media-s3
key: AWS_ACCESS_KEY_ID
- name: Sonarr__MediaCoverS3__SecretKey
valueFrom:
secretKeyRef:
name: arrstack-media-s3
key: AWS_SECRET_ACCESS_KEY
- name: Sonarr__MediaCoverS3__Bucket
value: arrstack-media
- name: Sonarr__MediaCoverS3__Prefix
value: sonarr
- name: Sonarr__MediaCoverS3__ForcePathStyle
value: "true"
- name: Sonarr__MediaCoverS3__CaCertPath
value: /etc/ssl/vault-ca/ca.crt
# Backup object store (shared arrstack-backups Ceph RGW bucket,
# per-app key prefix). Routes the periodic config+DB zip backups off
# the ephemeral /config so any replica can write and restore them.
- name: Sonarr__BackupS3__Endpoint
valueFrom:
secretKeyRef:
name: arrstack-backups-s3
key: S3_ENDPOINT
- name: Sonarr__BackupS3__AccessKey
valueFrom:
secretKeyRef:
name: arrstack-backups-s3
key: AWS_ACCESS_KEY_ID
- name: Sonarr__BackupS3__SecretKey
valueFrom:
secretKeyRef:
name: arrstack-backups-s3
key: AWS_SECRET_ACCESS_KEY
- name: Sonarr__BackupS3__Bucket
value: arrstack-backups
- name: Sonarr__BackupS3__Prefix
value: sonarr
- name: Sonarr__BackupS3__ForcePathStyle
value: "true"
- name: Sonarr__BackupS3__CaCertPath
value: /etc/ssl/vault-ca/ca.crt
command:
- sh
- -c
- |
set -eu
case "$API_KEY" in
"" | *[!0-9a-fA-F]*)
echo "apikey-init: API_KEY missing or not hex; refusing" >&2
exit 1
;;
esac
CFG=/config/config.xml
if [ ! -f "$CFG" ]; then
printf '<Config>\n <ApiKey>%s</ApiKey>\n</Config>\n' "$API_KEY" > "$CFG"
elif grep -q '<ApiKey>' "$CFG"; then
sed -i "s|<ApiKey>[^<]*</ApiKey>|<ApiKey>${API_KEY}</ApiKey>|" "$CFG"
else
sed -i "s|<Config>|<Config>\n <ApiKey>${API_KEY}</ApiKey>|" "$CFG"
fi
chown 1000:1000 "$CFG"
chmod 600 "$CFG"
echo "apikey-init: <ApiKey> enforced from Vault"
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 200m
memory: 64Mi
volumeMounts:
- name: config
mountPath: /config
containers:
- name: sonarr
image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/linuxserver/sonarr:4.0.19
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8989
protocol: TCP
env:
- name: PUID
value: "1000"
- name: PGID
value: "1000"
- name: TZ
value: Australia/Sydney
livenessProbe:
httpGet:
path: /sonarr/ping
path: /ping
port: http
initialDelaySeconds: 30
periodSeconds: 30
@@ -170,7 +96,7 @@ spec:
failureThreshold: 3
readinessProbe:
httpGet:
path: /sonarr/ping
path: /ping
port: http
initialDelaySeconds: 10
periodSeconds: 10
@@ -188,64 +114,10 @@ spec:
mountPath: /config
- name: media-tv
mountPath: /media/tv
- name: vault-ca
mountPath: /etc/ssl/vault-ca
readOnly: true
# exportarr sidecar: polls the local replica's API and exposes Prometheus
# metrics on :9707 (scraped by the sonarr-exportarr VMPodScrape).
- name: exportarr
image: artifactapi.k8s.syd1.au.unkin.net/ghcr/onedr0p/exportarr:v2.3.0
imagePullPolicy: IfNotPresent
args:
- sonarr
env:
- name: PORT
value: "9707"
# URL includes the /sonarr UrlBase (Sonarr__Server__UrlBase).
- name: URL
value: http://localhost:8989/sonarr
- name: APIKEY
valueFrom:
secretKeyRef:
name: sonarr-apikey
key: apitoken
ports:
- name: metrics
containerPort: 9707
protocol: TCP
livenessProbe:
httpGet:
path: /healthz
port: metrics
initialDelaySeconds: 15
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz
port: metrics
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 100m
memory: 128Mi
volumes:
- name: config
emptyDir: {}
persistentVolumeClaim:
claimName: sonarr-config
- name: media-tv
persistentVolumeClaim:
claimName: media-tv
# Estate CA for validating the Ceph RGW (s3.ceph.unkin.net) TLS cert.
- name: vault-ca
secret:
secretName: vault-ca-cert
items:
- key: ca.crt
path: ca.crt
+1 -2
View File
@@ -3,10 +3,9 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- pvc-config.yaml
- vaultstaticsecret.yaml
- configmap.yaml
- deployment.yaml
- service.yaml
- gateway.yaml
- httproute.yaml
- vmpodscrape.yaml
+4 -5
View File
@@ -1,7 +1,6 @@
---
# Sonarr /config. RWX on CephFS so all replicas share it (the -unkin2 fork keeps
# the database in Postgres; /config now holds only config.xml + MediaCover, which
# tolerate — and want — shared access). Retain: this is state.
# Sonarr config + SQLite DB. RWO on cephrbd (block) — the arr apps' SQLite
# does not tolerate CephFS locking. Retain: this is state.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
@@ -9,9 +8,9 @@ metadata:
namespace: arrstack
spec:
accessModes:
- ReadWriteMany
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: cephfs-raid5-retain
storageClassName: cephrbd-fast-retain
volumeMode: Filesystem
@@ -1,16 +0,0 @@
---
# Scrape the exportarr sidecar (:9707) on every sonarr pod. Picked up by the
# observability VMAgent (selectAllByDefault). Pod-level rather than
# VMServiceScrape because the sonarr Service doesn't expose the metrics port.
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMPodScrape
metadata:
name: sonarr-exportarr
namespace: arrstack
spec:
selector:
matchLabels:
app: sonarr
podMetricsEndpoints:
- port: metrics
path: /metrics
@@ -1,6 +0,0 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- valkeycluster.yaml
@@ -1,47 +0,0 @@
---
# Single shared HA Valkey for the arr apps (sonarr/radarr/prowlarr), managed by
# valkey-operator. It activates the fork's #14 Redis features: the SignalR
# backplane, the cross-replica cache-invalidation bus, and the distributed rate
# limiter. One cluster is safe for all three because each fork namespaces its
# keys and pub/sub channels by a per-app prefix (sonarr:ratelimit: /
# radarr:ratelimit: / prowlarr:ratelimit:), so their state never collides.
#
# Modeled on jellyfin-valkey: shards:1 + replicas:2 is one primary with two
# replicas in a single shard group (three ValkeyNodes total); losing the primary
# triggers an automatic failover so a node/pod loss no longer drops the shared
# state the app replicas coordinate through. The operator runs Valkey
# cluster-mode-enabled with protected-mode off and leaves the built-in `default`
# user passwordless, so clients connect with no auth/TLS; StackExchange.Redis
# seeds off the single service and auto-discovers topology plus failovers.
# scheduling.node.spread.shard:Required keeps the three nodes on distinct hosts,
# so one host loss removes at most one node; podDisruptionBudget.mode:Cluster
# lets the operator manage a quorum-aware PDB. Persistence is omitted (/data is an
# emptyDir): the coordination state is ephemeral (short TTLs / transient pub/sub),
# replication+failover already provide redundancy, and an operator-managed PVC
# cannot carry the k8up.io/backup:"false" annotation the namespace k8up Schedule
# needs to skip in-use RWO volumes.
apiVersion: valkey.io/v1alpha1
kind: ValkeyCluster
metadata:
name: arrstack-valkey
namespace: arrstack
spec:
shards: 1
replicas: 2
image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/valkey/valkey:9.0.0
exporter:
enabled: false
scheduling:
node:
spread:
shard:
mode: Required
podDisruptionBudget:
mode: Cluster
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
-1
View File
@@ -18,5 +18,4 @@ resources:
- ui-hpa.yaml
- vaultauth.yaml
- vaultstaticsecret.yaml
- vmpodscrape.yaml
- vpa.yaml
@@ -53,18 +53,4 @@ spec:
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
- name: metrics-exporter
image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/oliver006/redis_exporter:v1.89.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 9121
name: metrics
protocol: TCP
resources:
limits:
cpu: 100m
memory: 64Mi
requests:
cpu: 25m
memory: 32Mi
restartPolicy: Always
-16
View File
@@ -1,16 +0,0 @@
---
# Scrape the redis_exporter sidecar (:9121) on the redis pod. Picked up by the
# observability VMAgent (selectAllByDefault). Pod-level rather than
# VMServiceScrape because the redis Service doesn't expose the metrics port.
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMPodScrape
metadata:
name: redis-exporter
namespace: artifactapi
spec:
selector:
matchLabels:
app: redis
podMetricsEndpoints:
- port: metrics
path: /metrics
-1
View File
@@ -18,5 +18,4 @@ resources:
- redis-service.yaml
- vaultauth.yaml
- vaultstaticsecret.yaml
- vmpodscrape.yaml
- vpa.yaml
-14
View File
@@ -52,20 +52,6 @@ spec:
volumeMounts:
- mountPath: /data
name: redis-data
- name: metrics-exporter
image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/oliver006/redis_exporter:v1.89.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 9121
name: metrics
protocol: TCP
resources:
limits:
cpu: 100m
memory: 64Mi
requests:
cpu: 25m
memory: 32Mi
volumes:
- name: redis-data
persistentVolumeClaim:
-16
View File
@@ -1,16 +0,0 @@
---
# Scrape the redis_exporter sidecar (:9121) on the redis pod. Picked up by the
# observability VMAgent (selectAllByDefault). Pod-level rather than
# VMServiceScrape because the redis Service doesn't expose the metrics port.
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMPodScrape
metadata:
name: redis-exporter
namespace: authentik
spec:
selector:
matchLabels:
app: redis
podMetricsEndpoints:
- port: metrics
path: /metrics
@@ -117,18 +117,3 @@ spec:
# traefik-EXTERNAL (DMZ) gateway VIP; the arrproxy Gateway serves the
# arrstack.unkin.net front door (oauth2-proxy) there.
- 198.18.199.0
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: logviewer-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: logviewer
type: A
ttl: 600
values:
# traefik-internal gateway VIP; the logviewer Gateway serves
# logviewer.unkin.net there.
- 198.18.200.4
-1
View File
@@ -6,6 +6,5 @@ resources:
- namespace.yaml
- vaultauth.yaml
- vaultstaticsecret.yaml
- vaultstaticsecret-mediafs.yaml
- storageclass.yaml
- vmservicescrape.yaml
@@ -1,15 +0,0 @@
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: ceph-mediafs-secret
namespace: csi-cephfs
spec:
vaultAuthRef: ceph-csi-cephfs
mount: kv
type: kv-v2
path: service/kubernetes/au/syd1/csi/ceph-mediafs-secret
refreshAfter: 5m
destination:
name: ceph-mediafs-secret
create: true
-1
View File
@@ -10,7 +10,6 @@ resources:
- valkey-deployment.yaml
- valkey-pvc.yaml
- valkey-service.yaml
- vmpodscrape.yaml
- vaultauth.yaml
- vaultstaticsecret.yaml
- gateway.yaml
-20
View File
@@ -82,26 +82,6 @@ spec:
volumeMounts:
- mountPath: /data
name: data
- name: metrics-exporter
image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/oliver006/redis_exporter:v1.89.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 9121
name: metrics
protocol: TCP
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
capabilities:
drop:
- ALL
resources:
limits:
cpu: 100m
memory: 64Mi
requests:
cpu: 25m
memory: 32Mi
restartPolicy: Always
volumes:
- name: data
-16
View File
@@ -1,16 +0,0 @@
---
# Scrape the redis_exporter sidecar (:9121) on the gitea-valkey pod. Picked up
# by the observability VMAgent (selectAllByDefault). Pod-level rather than
# VMServiceScrape because the valkey Service doesn't expose the metrics port.
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMPodScrape
metadata:
name: valkey-exporter
namespace: gitea
spec:
selector:
matchLabels:
app: gitea-valkey
podMetricsEndpoints:
- port: metrics
path: /metrics
-13
View File
@@ -1,13 +0,0 @@
---
apiVersion: grafana.integreatly.org/v1beta1
kind: GrafanaDashboard
metadata:
name: redis
namespace: grafana
spec:
instanceSelector:
matchLabels:
dashboards: "grafana"
resyncPeriod: 5m
allowCrossNamespaceImport: false
gzipJson: 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
-13
View File
@@ -1,13 +0,0 @@
---
apiVersion: grafana.integreatly.org/v1beta1
kind: GrafanaDashboard
metadata:
name: traefik
namespace: grafana
spec:
instanceSelector:
matchLabels:
dashboards: "grafana"
resyncPeriod: 5m
allowCrossNamespaceImport: false
gzipJson: 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
-2
View File
@@ -23,8 +23,6 @@ resources:
- dashboards/nzbget.yaml
- dashboards/postgresql-database.yaml
- dashboards/puppet-report.yaml
- dashboards/redis.yaml
- dashboards/traefik.yaml
- dashboards/victorialogs-cluster.yaml
- dashboards/victoriametrics-cluster.yaml
- dashboards/victoriametrics-vmagent.yaml
-1
View File
@@ -21,6 +21,5 @@ resources:
- pdb.yaml
- service.yaml
- valkey.yaml
- vmpodscrape.yaml
- gateway.yaml
- httproute.yaml
+1 -7
View File
@@ -25,14 +25,8 @@ spec:
shards: 1
replicas: 2
image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/valkey/valkey:9.0.0
# redis_exporter sidecar (:9121, port name `metrics`) on every ValkeyNode pod;
# the operator manages a dedicated _exporter ACL user for it. Image overridden
# from the operator default (bare dockerhub oliver006/redis_exporter:v1.80.0)
# to the artifactapi-proxied pin. Scraped by the valkey-exporter VMPodScrape
# in vmpodscrape.yaml alongside this file.
exporter:
enabled: true
image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/oliver006/redis_exporter:v1.89.0
enabled: false
scheduling:
node:
spread:
-19
View File
@@ -1,19 +0,0 @@
---
# Scrape the operator-injected redis_exporter sidecar (:9121, port name
# `metrics`) on the jellyfin-valkey ValkeyNode pods. The valkey-operator gives
# its pods fixed labels only (no pod-label passthrough on the ValkeyCluster CR),
# so select on the operator-managed labels. Picked up by the observability
# VMAgent (selectAllByDefault).
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMPodScrape
metadata:
name: valkey-exporter
namespace: jellyfin
spec:
selector:
matchLabels:
app.kubernetes.io/managed-by: valkey-operator
app.kubernetes.io/component: valkey-node
podMetricsEndpoints:
- port: metrics
path: /metrics
-1
View File
@@ -16,7 +16,6 @@ resources:
- services.yaml
- vaultauth.yaml
- vaultstaticsecret.yaml
- vmpodscrape.yaml
- vpa.yaml
configMapGenerator:
-14
View File
@@ -60,20 +60,6 @@ spec:
- mountPath: /data
mountPropagation: None
name: data
- name: metrics-exporter
image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/oliver006/redis_exporter:v1.89.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 9121
name: metrics
protocol: TCP
resources:
limits:
cpu: 100m
memory: 64Mi
requests:
cpu: 25m
memory: 32Mi
restartPolicy: Always
volumes:
- name: data
-16
View File
@@ -1,16 +0,0 @@
---
# Scrape the redis_exporter sidecar (:9121) on the redis pod. Picked up by the
# observability VMAgent (selectAllByDefault). Pod-level rather than
# VMServiceScrape because the redis Service doesn't expose the metrics port.
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMPodScrape
metadata:
name: redis-exporter
namespace: litellm
spec:
selector:
matchLabels:
app: redis
podMetricsEndpoints:
- port: metrics
path: /metrics
@@ -29,26 +29,9 @@ spec:
default/networks/ip:
- "127.0.0.1"
- "::1"
# Read-only user for CLI tools + the logviewer UI. Hash sourced from the
# Vault-synced clickhouse-logreader Secret, same mechanism as vector.
# Scoped to the logs database only (unlike vector, which bootstraps it).
logreader/password_sha256_hex:
valueFrom:
secretKeyRef:
name: clickhouse-logreader
key: password_sha256_hex
logreader/networks/ip:
- "::/0"
logreader/profile: readonly
logreader/quota: default
logreader/allow_databases/database:
- "logs"
profiles:
default/max_memory_usage: "10000000000"
default/max_execution_time: "120"
readonly/readonly: "2"
readonly/max_memory_usage: "10000000000"
readonly/max_execution_time: "120"
clusters:
- name: logs
layout:
-1
View File
@@ -15,7 +15,6 @@ resources:
- serviceaccount_logarchiver.yaml
- configmap_logarchiver.yaml
- deployment_logarchiver.yaml
- logviewer
# Vector pipelines are the single source of truth (also validated by
# `vector test` in CI). Mounted into each tier via `existingConfigMaps`.
@@ -1,80 +0,0 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: logviewer
namespace: logging
annotations:
secret.reloader.stakater.com/reload: "clickhouse-logreader"
spec:
replicas: 1
selector:
matchLabels:
app: logviewer
strategy:
type: Recreate
template:
metadata:
labels:
app: logviewer
spec:
serviceAccountName: default
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: logviewer
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/logviewer:v0.1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
name: http
protocol: TCP
env:
- name: CH_URL
value: http://clickhouse-logs.logging.svc.cluster.local:8123
- name: CH_USER
valueFrom:
secretKeyRef:
name: clickhouse-logreader
key: username
- name: CH_PASSWORD
valueFrom:
secretKeyRef:
name: clickhouse-logreader
key: password
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
restartPolicy: Always
-38
View File
@@ -1,38 +0,0 @@
---
# Internal front for the logviewer UI (cf. mediamover/pdbmux).
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
labels:
traefik.io/instance: internal
annotations:
cert-manager.io/cluster-issuer: vault-issuer
cert-manager.io/common-name: logviewer.unkin.net
cert-manager.io/private-key-size: "4096"
external-dns.alpha.kubernetes.io/hostname: logviewer.unkin.net
external-dns.alpha.kubernetes.io/target: 198.18.200.4
name: logviewer
namespace: logging
spec:
gatewayClassName: traefik-internal
listeners:
- allowedRoutes:
namespaces:
from: Same
hostname: logviewer.unkin.net
name: http
port: 80
protocol: HTTP
- allowedRoutes:
namespaces:
from: Same
hostname: logviewer.unkin.net
name: https
port: 443
protocol: HTTPS
tls:
certificateRefs:
- group: ""
kind: Secret
name: logviewer-tls
mode: Terminate
@@ -1,49 +0,0 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: logviewer-http-redirect
namespace: logging
spec:
hostnames:
- logviewer.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: logviewer
sectionName: http
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
matches:
- path:
type: PathPrefix
value: /
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: logviewer
namespace: logging
spec:
hostnames:
- logviewer.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: logviewer
sectionName: https
rules:
- backendRefs:
- group: ""
kind: Service
name: logviewer-oauth2
port: 80
weight: 1
matches:
- path:
type: PathPrefix
value: /
@@ -1,12 +0,0 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- vaultstaticsecret.yaml
- deployment.yaml
- oauth2-proxy-configmap.yaml
- oauth2-proxy-deployment.yaml
- service.yaml
- gateway.yaml
- httproute.yaml
@@ -1,32 +0,0 @@
---
# Non-secret oauth2-proxy configuration (client_id/secret/cookie_secret come
# from the logviewer-oauth-credentials Secret). Single auth front for the
# logviewer UI: everything requires an Authentik session in akP-logviewer-admin.
apiVersion: v1
kind: ConfigMap
metadata:
name: logviewer-oauth2-env
namespace: logging
data:
OAUTH2_PROXY_HTTP_ADDRESS: "0.0.0.0:4180"
OAUTH2_PROXY_PROVIDER: "oidc"
# Authentik logviewer app discovery issuer (terraform-authentik PR #21).
OAUTH2_PROXY_OIDC_ISSUER_URL: "https://identity.unkin.net/application/o/logviewer/"
OAUTH2_PROXY_REDIRECT_URL: "https://logviewer.unkin.net/oauth2/callback"
OAUTH2_PROXY_UPSTREAMS: "http://logviewer.logging.svc.cluster.local:8080/"
OAUTH2_PROXY_SCOPE: "openid email profile ak_groups"
# Populate session.Groups from the Authentik ak_groups claim.
OAUTH2_PROXY_OIDC_GROUPS_CLAIM: "ak_groups"
OAUTH2_PROXY_ALLOWED_GROUPS: "akP-logviewer-admin"
OAUTH2_PROXY_PASS_USER_HEADERS: "true"
OAUTH2_PROXY_EMAIL_DOMAINS: "*"
# Authentik hardcodes email_verified=false in the id_token; authorization is
# enforced via ak_groups, so accepting the unverified email is safe.
OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL: "true"
OAUTH2_PROXY_COOKIE_SECURE: "true"
OAUTH2_PROXY_COOKIE_DOMAINS: "logviewer.unkin.net"
OAUTH2_PROXY_WHITELIST_DOMAINS: "logviewer.unkin.net"
OAUTH2_PROXY_REVERSE_PROXY: "true"
OAUTH2_PROXY_PROVIDER_CA_FILES: "/etc/ssl/combined/ca-certificates.crt"
OAUTH2_PROXY_CODE_CHALLENGE_METHOD: "S256"
OAUTH2_PROXY_SKIP_PROVIDER_BUTTON: "true"
@@ -1,132 +0,0 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: logviewer-oauth2
namespace: logging
annotations:
configmap.reloader.stakater.com/auto: "true"
secret.reloader.stakater.com/reload: "logviewer-oauth-credentials,vault-ca-cert"
spec:
replicas: 1
selector:
matchLabels:
app: logviewer-oauth2
strategy:
rollingUpdate:
maxUnavailable: 1
type: RollingUpdate
template:
metadata:
labels:
app: logviewer-oauth2
spec:
serviceAccountName: default
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
initContainers:
# identity.unkin.net serves a Vault-PKI cert; combine the system roots
# with the internal CA so oauth2-proxy's OIDC HTTP client trusts it.
- name: combine-certs
image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/library/alpine:3
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- cat /etc/ssl/certs/ca-certificates.crt /custom-ca/ca.crt > /combined-certs/ca-certificates.crt
volumeMounts:
- name: vault-ca-cert
mountPath: /custom-ca
readOnly: true
- name: combined-certs
mountPath: /combined-certs
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 200m
memory: 64Mi
containers:
- name: oauth2-proxy
image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.3
imagePullPolicy: IfNotPresent
ports:
- containerPort: 4180
name: http
protocol: TCP
envFrom:
- configMapRef:
name: logviewer-oauth2-env
optional: false
env:
- name: OAUTH2_PROXY_CLIENT_ID
valueFrom:
secretKeyRef:
name: logviewer-oauth-credentials
key: client_id
- name: OAUTH2_PROXY_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: logviewer-oauth-credentials
key: client_secret
- name: OAUTH2_PROXY_COOKIE_SECRET
valueFrom:
secretKeyRef:
name: logviewer-oauth-credentials
key: cookie_secret
volumeMounts:
- name: combined-certs
mountPath: /etc/ssl/combined
readOnly: true
livenessProbe:
httpGet:
path: /ping
port: http
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
volumes:
- name: vault-ca-cert
secret:
secretName: vault-ca-cert
items:
- key: ca.crt
path: ca.crt
- name: combined-certs
emptyDir: {}
restartPolicy: Always
-36
View File
@@ -1,36 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: logviewer
namespace: logging
spec:
internalTrafficPolicy: Cluster
ports:
- name: http
port: 8080
protocol: TCP
targetPort: http
selector:
app: logviewer
sessionAffinity: None
type: ClusterIP
---
# Front-door entry Service: the HTTPRoute for logviewer.unkin.net targets this;
# all traffic enters via oauth2-proxy.
apiVersion: v1
kind: Service
metadata:
name: logviewer-oauth2
namespace: logging
spec:
internalTrafficPolicy: Cluster
ports:
- name: http
port: 80
protocol: TCP
targetPort: http
selector:
app: logviewer-oauth2
sessionAffinity: None
type: ClusterIP
@@ -1,21 +0,0 @@
---
# Authentik OIDC client for logviewer (client_id, client_secret, cookie_secret)
# seeded at kv/kubernetes/namespace/logging/default/oauth-credentials; the
# logging/default templated policy already grants read, so no terraform-vault
# change is needed.
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: logviewer-oauth-credentials
namespace: logging
spec:
destination:
create: true
name: logviewer-oauth-credentials
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/logging/default/oauth-credentials
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
-23
View File
@@ -27,29 +27,6 @@ spec:
type: kv-v2
vaultAuthRef: default
---
# ClickHouse credentials for the read-only `logreader` user (CLI tools +
# logviewer UI). Seeded the same way as clickhouse-credentials above:
# PW=$(openssl rand -hex 24)
# HASH=$(printf '%s' "$PW" | sha256sum | cut -d' ' -f1)
# vault kv put kv/kubernetes/namespace/logging/default/clickhouse-logreader \
# username=logreader password="$PW" password_sha256_hex="$HASH"
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: clickhouse-logreader
namespace: logging
spec:
destination:
create: true
name: clickhouse-logreader
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/logging/default/clickhouse-logreader
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
---
# NATS JetStream auth. Distinct passwords for the producer (edge), consumer
# (transform tier + archiver) and admin (bootstrap Job) users. Seed once:
# for k in admin producer consumer; do declare P_$k=$(openssl rand -base64 24); done
-1
View File
@@ -10,7 +10,6 @@ resources:
- valkey-deployment.yaml
- valkey-pvc.yaml
- valkey-service.yaml
- vmpodscrape.yaml
- vaultauth.yaml
- vaultstaticsecret.yaml
- gateway.yaml
-20
View File
@@ -82,26 +82,6 @@ spec:
volumeMounts:
- mountPath: /data
name: data
- name: metrics-exporter
image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/oliver006/redis_exporter:v1.89.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 9121
name: metrics
protocol: TCP
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
capabilities:
drop:
- ALL
resources:
limits:
cpu: 100m
memory: 64Mi
requests:
cpu: 25m
memory: 32Mi
restartPolicy: Always
volumes:
- name: data
-16
View File
@@ -1,16 +0,0 @@
---
# Scrape the redis_exporter sidecar (:9121) on the netbox-valkey pod. Picked up
# by the observability VMAgent (selectAllByDefault). Pod-level rather than
# VMServiceScrape because the valkey Service doesn't expose the metrics port.
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMPodScrape
metadata:
name: valkey-exporter
namespace: netbox
spec:
selector:
matchLabels:
app: netbox-valkey
podMetricsEndpoints:
- port: metrics
path: /metrics
@@ -1,201 +0,0 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: kube-state-metrics
namespace: observability
labels:
app.kubernetes.io/name: kube-state-metrics
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kube-state-metrics
labels:
app.kubernetes.io/name: kube-state-metrics
rules:
- apiGroups: [""]
resources:
- configmaps
- secrets
- nodes
- pods
- services
- serviceaccounts
- resourcequotas
- replicationcontrollers
- limitranges
- persistentvolumeclaims
- persistentvolumes
- namespaces
- endpoints
verbs: [list, watch]
- apiGroups: [apps]
resources:
- statefulsets
- daemonsets
- deployments
- replicasets
verbs: [list, watch]
- apiGroups: [batch]
resources:
- cronjobs
- jobs
verbs: [list, watch]
- apiGroups: [autoscaling]
resources:
- horizontalpodautoscalers
verbs: [list, watch]
- apiGroups: [authentication.k8s.io]
resources:
- tokenreviews
verbs: [create]
- apiGroups: [authorization.k8s.io]
resources:
- subjectaccessreviews
verbs: [create]
- apiGroups: [policy]
resources:
- poddisruptionbudgets
verbs: [list, watch]
- apiGroups: [certificates.k8s.io]
resources:
- certificatesigningrequests
verbs: [list, watch]
- apiGroups: [discovery.k8s.io]
resources:
- endpointslices
verbs: [list, watch]
- apiGroups: [storage.k8s.io]
resources:
- storageclasses
- volumeattachments
verbs: [list, watch]
- apiGroups: [admissionregistration.k8s.io]
resources:
- mutatingwebhookconfigurations
- validatingwebhookconfigurations
verbs: [list, watch]
- apiGroups: [networking.k8s.io]
resources:
- networkpolicies
- ingressclasses
- ingresses
verbs: [list, watch]
- apiGroups: [coordination.k8s.io]
resources:
- leases
verbs: [list, watch]
- apiGroups: [rbac.authorization.k8s.io]
resources:
- clusterrolebindings
- clusterroles
- rolebindings
- roles
verbs: [list, watch]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kube-state-metrics
labels:
app.kubernetes.io/name: kube-state-metrics
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kube-state-metrics
subjects:
- kind: ServiceAccount
name: kube-state-metrics
namespace: observability
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: kube-state-metrics
namespace: observability
labels:
app.kubernetes.io/name: kube-state-metrics
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: kube-state-metrics
template:
metadata:
labels:
app.kubernetes.io/name: kube-state-metrics
spec:
serviceAccountName: kube-state-metrics
automountServiceAccountToken: true
containers:
- name: kube-state-metrics
image: registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.20.0
ports:
- name: http-metrics
containerPort: 8080
- name: telemetry
containerPort: 8081
livenessProbe:
httpGet:
path: /livez
port: http-metrics
initialDelaySeconds: 5
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /readyz
port: telemetry
initialDelaySeconds: 5
timeoutSeconds: 5
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65534
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
name: kube-state-metrics
namespace: observability
labels:
app.kubernetes.io/name: kube-state-metrics
spec:
clusterIP: None
selector:
app.kubernetes.io/name: kube-state-metrics
ports:
- name: http-metrics
port: 8080
targetPort: http-metrics
- name: telemetry
port: 8081
targetPort: telemetry
---
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMServiceScrape
metadata:
name: kube-state-metrics
namespace: observability
spec:
# jobLabel yields job="kube-state-metrics" as kube-prometheus selectors expect.
jobLabel: app.kubernetes.io/name
selector:
matchLabels:
app.kubernetes.io/name: kube-state-metrics
endpoints:
- port: http-metrics
honorLabels: true
- port: telemetry
@@ -6,10 +6,6 @@ resources:
- namespace.yaml
- vmcluster.yaml
- vmagent.yaml
- vmalert.yaml
- vmrule-k8s.yaml
- kube-state-metrics.yaml
- gateway.yaml
- httproute.yaml
- vmpodscrape-cnpg.yaml
- vmpodscrape-traefik.yaml
+6 -12
View File
@@ -49,8 +49,7 @@ spec:
- __meta_kubernetes_endpoint_port_name
action: keep
regex: default;kubernetes;https
# Only the node name goes onto series: a blanket node-label labelmap pushes past vminsert's 40-label limit and drops everything.
- job_name: "kubelet"
- job_name: "kubernetes-nodes"
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
@@ -59,11 +58,9 @@ spec:
kubernetes_sd_configs:
- role: node
relabel_configs:
- source_labels: [__meta_kubernetes_node_name]
target_label: node
- source_labels: [__metrics_path__]
target_label: metrics_path
- job_name: "kubelet-cadvisor"
- action: labelmap
regex: __meta_kubernetes_node_label_(.+)
- job_name: "kubernetes-nodes-cadvisor"
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
@@ -73,13 +70,10 @@ spec:
- role: node
metrics_path: /metrics/cadvisor
relabel_configs:
- source_labels: [__meta_kubernetes_node_name]
target_label: node
- action: labelmap
regex: __meta_kubernetes_node_label_(.+)
- source_labels: [__metrics_path__]
target_label: metrics_path
# job_name must be unique; kube-prometheus selectors expect job="kubelet"
- target_label: job
replacement: kubelet
metric_relabel_configs:
- action: replace
source_labels: [pod]
-27
View File
@@ -1,27 +0,0 @@
---
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMAlert
metadata:
name: main
namespace: observability
spec:
replicaCount: 1
selectAllByDefault: true
evaluationInterval: 30s
datasource:
url: http://vmselect-main.observability.svc.cluster.local:8481/select/0/prometheus/
remoteWrite:
url: http://vminsert-main.observability.svc.cluster.local:8480/insert/0/prometheus/
remoteRead:
url: http://vmselect-main.observability.svc.cluster.local:8481/select/0/prometheus/
extraArgs:
loggerFormat: json
# recording rules only for now; no alertmanager deployed
notifier.blackhole: "true"
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
@@ -1,23 +0,0 @@
---
# Scrape metrics (:9100) from the traefik-internal and traefik-external gateway
# pods. Picked up by the observability VMAgent (selectAllByDefault). The
# app.kubernetes.io/instance pod label is kept as traefik_instance so series
# from the two releases stay distinguishable.
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMPodScrape
metadata:
name: traefik
namespace: observability
spec:
namespaceSelector:
matchNames:
- traefik-system
selector:
matchLabels:
app.kubernetes.io/name: traefik
podMetricsEndpoints:
- port: metrics
path: /metrics
relabelConfigs:
- sourceLabels: [__meta_kubernetes_pod_label_app_kubernetes_io_instance]
targetLabel: traefik_instance
-133
View File
@@ -1,133 +0,0 @@
---
# kube-prometheus k8s.rules recording rules (dashboard queries depend on these).
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMRule
metadata:
name: k8s-rules
namespace: observability
spec:
groups:
- name: k8s.rules
rules:
- record: node_namespace_pod_container:container_cpu_usage_seconds_total:sum_irate
expr: |
sum by (cluster, namespace, pod, container) (
irate(container_cpu_usage_seconds_total{job="kubelet", metrics_path="/metrics/cadvisor", image!=""}[5m])
) * on (cluster, namespace, pod) group_left(node) topk by (cluster, namespace, pod) (
1, max by (cluster, namespace, pod, node) (kube_pod_info{node!=""})
)
- record: node_namespace_pod_container:container_memory_working_set_bytes
expr: |
container_memory_working_set_bytes{job="kubelet", metrics_path="/metrics/cadvisor", image!=""}
* on (cluster, namespace, pod) group_left(node) topk by (cluster, namespace, pod) (
1, max by (cluster, namespace, pod, node) (kube_pod_info{node!=""})
)
- record: node_namespace_pod_container:container_memory_rss
expr: |
container_memory_rss{job="kubelet", metrics_path="/metrics/cadvisor", image!=""}
* on (cluster, namespace, pod) group_left(node) topk by (cluster, namespace, pod) (
1, max by (cluster, namespace, pod, node) (kube_pod_info{node!=""})
)
- record: node_namespace_pod_container:container_memory_cache
expr: |
container_memory_cache{job="kubelet", metrics_path="/metrics/cadvisor", image!=""}
* on (cluster, namespace, pod) group_left(node) topk by (cluster, namespace, pod) (
1, max by (cluster, namespace, pod, node) (kube_pod_info{node!=""})
)
- record: node_namespace_pod_container:container_memory_swap
expr: |
container_memory_swap{job="kubelet", metrics_path="/metrics/cadvisor", image!=""}
* on (cluster, namespace, pod) group_left(node) topk by (cluster, namespace, pod) (
1, max by (cluster, namespace, pod, node) (kube_pod_info{node!=""})
)
- record: namespace_memory:kube_pod_container_resource_requests:sum
expr: |
sum by (namespace, cluster) (
sum by (namespace, pod, cluster) (
max by (namespace, pod, container, cluster) (
kube_pod_container_resource_requests{resource="memory",job="kube-state-metrics"}
) * on (namespace, pod, cluster) group_left() max by (namespace, pod, cluster) (
kube_pod_status_phase{phase=~"Pending|Running"} == 1
)
)
)
- record: namespace_cpu:kube_pod_container_resource_requests:sum
expr: |
sum by (namespace, cluster) (
sum by (namespace, pod, cluster) (
max by (namespace, pod, container, cluster) (
kube_pod_container_resource_requests{resource="cpu",job="kube-state-metrics"}
) * on (namespace, pod, cluster) group_left() max by (namespace, pod, cluster) (
kube_pod_status_phase{phase=~"Pending|Running"} == 1
)
)
)
- record: namespace_memory:kube_pod_container_resource_limits:sum
expr: |
sum by (namespace, cluster) (
sum by (namespace, pod, cluster) (
max by (namespace, pod, container, cluster) (
kube_pod_container_resource_limits{resource="memory",job="kube-state-metrics"}
) * on (namespace, pod, cluster) group_left() max by (namespace, pod, cluster) (
kube_pod_status_phase{phase=~"Pending|Running"} == 1
)
)
)
- record: namespace_cpu:kube_pod_container_resource_limits:sum
expr: |
sum by (namespace, cluster) (
sum by (namespace, pod, cluster) (
max by (namespace, pod, container, cluster) (
kube_pod_container_resource_limits{resource="cpu",job="kube-state-metrics"}
) * on (namespace, pod, cluster) group_left() max by (namespace, pod, cluster) (
kube_pod_status_phase{phase=~"Pending|Running"} == 1
)
)
)
- record: namespace_workload_pod:kube_pod_owner:relabel
labels:
workload_type: deployment
expr: |
max by (cluster, namespace, workload, pod) (
label_replace(
label_replace(
kube_pod_owner{job="kube-state-metrics", owner_kind="ReplicaSet"},
"replicaset", "$1", "owner_name", "(.*)"
) * on (replicaset, namespace) group_left(owner_name) topk by (replicaset, namespace) (
1, max by (replicaset, namespace, owner_name) (
kube_replicaset_owner{job="kube-state-metrics"}
)
),
"workload", "$1", "owner_name", "(.*)"
)
)
- record: namespace_workload_pod:kube_pod_owner:relabel
labels:
workload_type: daemonset
expr: |
max by (cluster, namespace, workload, pod) (
label_replace(
kube_pod_owner{job="kube-state-metrics", owner_kind="DaemonSet"},
"workload", "$1", "owner_name", "(.*)"
)
)
- record: namespace_workload_pod:kube_pod_owner:relabel
labels:
workload_type: statefulset
expr: |
max by (cluster, namespace, workload, pod) (
label_replace(
kube_pod_owner{job="kube-state-metrics", owner_kind="StatefulSet"},
"workload", "$1", "owner_name", "(.*)"
)
)
- record: namespace_workload_pod:kube_pod_owner:relabel
labels:
workload_type: job
expr: |
max by (cluster, namespace, workload, pod) (
label_replace(
kube_pod_owner{job="kube-state-metrics", owner_kind="Job"},
"workload", "$1", "owner_name", "(.*)"
)
)
@@ -29,11 +29,6 @@ providers:
nativeLBByDefault: false
labelSelector: "traefik.io/instance=external"
metrics:
prometheus:
addEntryPointsLabels: true
addRoutersLabels: true
logs:
access:
enabled: true
@@ -29,11 +29,6 @@ providers:
nativeLBByDefault: false
labelSelector: "traefik.io/instance=internal"
metrics:
prometheus:
addEntryPointsLabels: true
addRoutersLabels: true
logs:
access:
enabled: true