389 lines
11 KiB
YAML
389 lines
11 KiB
YAML
{{- if not .Values.persistence.existingClaim }}
|
|
apiVersion: v1
|
|
kind: PersistentVolumeClaim
|
|
metadata:
|
|
name: plex-config
|
|
namespace: {{ .Release.Namespace }}
|
|
labels:
|
|
app.kubernetes.io/name: plex
|
|
app.kubernetes.io/component: state
|
|
spec:
|
|
accessModes:
|
|
- ReadWriteOnce
|
|
resources:
|
|
requests:
|
|
storage: {{ .Values.persistence.size }}
|
|
{{- with .Values.persistence.storageClass }}
|
|
storageClassName: {{ . }}
|
|
{{- end }}
|
|
{{- end }}
|
|
---
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: plex-scripts
|
|
namespace: {{ .Release.Namespace }}
|
|
labels:
|
|
app.kubernetes.io/name: plex
|
|
data:
|
|
check-media.sh: |
|
|
#!/bin/sh
|
|
set -eu
|
|
filesystem_magic=$(stat -f -c '%t' {{ .Values.media.mountPath }})
|
|
if [ "${filesystem_magic}" != ca451a4e ]; then
|
|
echo "Expected bcachefs at {{ .Values.media.mountPath }}, found filesystem magic ${filesystem_magic}" >&2
|
|
exit 1
|
|
fi
|
|
test -d {{ .Values.media.mountPath }}/Videos
|
|
extract-token.sh: |
|
|
#!/bin/sh
|
|
set -eu
|
|
preferences='/config/Library/Application Support/Plex Media Server/Preferences.xml'
|
|
test -s "${preferences}"
|
|
token=$(sed -n 's/.*PlexOnlineToken="\([^"]*\)".*/\1/p' "${preferences}")
|
|
if [ -z "${token}" ]; then
|
|
echo 'PlexOnlineToken is missing from Preferences.xml' >&2
|
|
exit 1
|
|
fi
|
|
umask 077
|
|
printf '%s' "${token}" > /token/plex-token
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: plex
|
|
namespace: {{ .Release.Namespace }}
|
|
labels:
|
|
app.kubernetes.io/name: plex
|
|
app.kubernetes.io/component: server
|
|
annotations:
|
|
deployment.brunner.ninja/revision: manual
|
|
spec:
|
|
replicas: 1
|
|
progressDeadlineSeconds: 3900
|
|
strategy:
|
|
type: Recreate
|
|
selector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: plex
|
|
app.kubernetes.io/component: server
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: plex
|
|
app.kubernetes.io/component: server
|
|
spec:
|
|
automountServiceAccountToken: false
|
|
hostNetwork: true
|
|
dnsPolicy: ClusterFirstWithHostNet
|
|
nodeSelector:
|
|
{{- toYaml .Values.nodeSelector | nindent 8 }}
|
|
tolerations:
|
|
{{- toYaml .Values.tolerations | nindent 8 }}
|
|
terminationGracePeriodSeconds: 120
|
|
securityContext:
|
|
seccompProfile:
|
|
type: RuntimeDefault
|
|
initContainers:
|
|
- name: media-ready
|
|
image: docker.io/library/alpine:3.24.2
|
|
imagePullPolicy: IfNotPresent
|
|
command:
|
|
- /tools/check-media.sh
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
capabilities:
|
|
drop:
|
|
- ALL
|
|
readOnlyRootFilesystem: true
|
|
runAsNonRoot: true
|
|
runAsUser: 65534
|
|
runAsGroup: 65534
|
|
volumeMounts:
|
|
- name: media
|
|
mountPath: {{ .Values.media.mountPath }}
|
|
readOnly: true
|
|
- name: media-ready
|
|
mountPath: /media-ready
|
|
readOnly: true
|
|
- name: tools
|
|
mountPath: /tools
|
|
readOnly: true
|
|
- name: exporter-token
|
|
image: docker.io/library/alpine:3.24.2
|
|
imagePullPolicy: IfNotPresent
|
|
command:
|
|
- /tools/extract-token.sh
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
capabilities:
|
|
drop:
|
|
- ALL
|
|
readOnlyRootFilesystem: true
|
|
runAsNonRoot: true
|
|
runAsUser: 964
|
|
runAsGroup: 964
|
|
volumeMounts:
|
|
- name: config
|
|
mountPath: /config
|
|
readOnly: true
|
|
- name: exporter-token
|
|
mountPath: /token
|
|
- name: tools
|
|
mountPath: /tools
|
|
readOnly: true
|
|
containers:
|
|
- name: plex
|
|
image: {{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}
|
|
imagePullPolicy: IfNotPresent
|
|
env:
|
|
- name: TZ
|
|
value: {{ .Values.timezone | quote }}
|
|
- name: PLEX_UID
|
|
value: {{ .Values.plexUid | quote }}
|
|
- name: PLEX_GID
|
|
value: {{ .Values.plexGid | quote }}
|
|
- name: CHANGE_CONFIG_DIR_OWNERSHIP
|
|
value: "false"
|
|
ports:
|
|
- name: http
|
|
containerPort: 32400
|
|
protocol: TCP
|
|
startupProbe:
|
|
httpGet:
|
|
path: /identity
|
|
port: http
|
|
failureThreshold: 720
|
|
periodSeconds: 5
|
|
timeoutSeconds: 3
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /identity
|
|
port: http
|
|
failureThreshold: 3
|
|
periodSeconds: 10
|
|
timeoutSeconds: 3
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /identity
|
|
port: http
|
|
failureThreshold: 3
|
|
periodSeconds: 30
|
|
timeoutSeconds: 5
|
|
resources:
|
|
{{- toYaml .Values.resources | nindent 12 }}
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
volumeMounts:
|
|
- name: config
|
|
mountPath: /config
|
|
- name: media
|
|
mountPath: {{ .Values.media.mountPath }}
|
|
readOnly: true
|
|
- name: transcode
|
|
mountPath: /transcode
|
|
- name: plex-exporter
|
|
image: ghcr.io/cplieger/plex-exporter:v4.4.0
|
|
imagePullPolicy: IfNotPresent
|
|
env:
|
|
- name: PLEX_URL
|
|
value: http://127.0.0.1:32400
|
|
- name: PLEX_TOKEN_FILE
|
|
value: /token/plex-token
|
|
ports:
|
|
- name: metrics
|
|
containerPort: 9594
|
|
protocol: TCP
|
|
startupProbe:
|
|
httpGet:
|
|
path: /api/health
|
|
port: metrics
|
|
failureThreshold: 60
|
|
periodSeconds: 5
|
|
timeoutSeconds: 3
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /api/health
|
|
port: metrics
|
|
failureThreshold: 3
|
|
periodSeconds: 10
|
|
timeoutSeconds: 3
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /api/health
|
|
port: metrics
|
|
failureThreshold: 3
|
|
periodSeconds: 30
|
|
timeoutSeconds: 5
|
|
resources:
|
|
requests:
|
|
cpu: 10m
|
|
memory: 32Mi
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
capabilities:
|
|
drop:
|
|
- ALL
|
|
readOnlyRootFilesystem: true
|
|
runAsNonRoot: true
|
|
runAsUser: 964
|
|
runAsGroup: 964
|
|
volumeMounts:
|
|
- name: exporter-token
|
|
mountPath: /token
|
|
readOnly: true
|
|
- name: exporter-tmp
|
|
mountPath: /tmp
|
|
volumes:
|
|
- name: config
|
|
persistentVolumeClaim:
|
|
claimName: {{ default "plex-config" .Values.persistence.existingClaim }}
|
|
- name: media
|
|
hostPath:
|
|
path: {{ .Values.media.hostPath }}
|
|
type: Directory
|
|
- name: media-ready
|
|
hostPath:
|
|
path: {{ .Values.media.hostPath }}/Videos
|
|
type: Directory
|
|
- name: transcode
|
|
emptyDir: {}
|
|
- name: exporter-token
|
|
emptyDir:
|
|
medium: Memory
|
|
sizeLimit: 1Mi
|
|
- name: exporter-tmp
|
|
emptyDir:
|
|
medium: Memory
|
|
sizeLimit: 8Mi
|
|
- name: tools
|
|
configMap:
|
|
name: plex-scripts
|
|
defaultMode: 0555
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: plex
|
|
namespace: {{ .Release.Namespace }}
|
|
labels:
|
|
app.kubernetes.io/name: plex
|
|
app.kubernetes.io/component: service
|
|
spec:
|
|
selector:
|
|
app.kubernetes.io/name: plex
|
|
app.kubernetes.io/component: server
|
|
ports:
|
|
- name: http
|
|
port: 32400
|
|
targetPort: http
|
|
protocol: TCP
|
|
- name: metrics
|
|
port: 9594
|
|
targetPort: metrics
|
|
protocol: TCP
|
|
{{- if .Values.ingress.enabled }}
|
|
---
|
|
apiVersion: traefik.io/v1alpha1
|
|
kind: IngressRoute
|
|
metadata:
|
|
name: {{ default "plex" .Values.ingress.name }}
|
|
namespace: {{ .Release.Namespace }}
|
|
labels:
|
|
app.kubernetes.io/name: plex
|
|
spec:
|
|
entryPoints:
|
|
- {{ .Values.ingress.entryPoint }}
|
|
routes:
|
|
- kind: Rule
|
|
match: Host(`{{ required "ingress.host is required" .Values.ingress.host }}`)
|
|
services:
|
|
- kind: Service
|
|
name: plex
|
|
port: http
|
|
passHostHeader: true
|
|
{{- end }}
|
|
{{- if .Values.monitoring.enabled }}
|
|
---
|
|
apiVersion: monitoring.coreos.com/v1
|
|
kind: ServiceMonitor
|
|
metadata:
|
|
name: plex
|
|
namespace: {{ .Release.Namespace }}
|
|
labels:
|
|
app.kubernetes.io/name: plex
|
|
spec:
|
|
selector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: plex
|
|
app.kubernetes.io/component: service
|
|
endpoints:
|
|
- port: metrics
|
|
path: /metrics
|
|
interval: 30s
|
|
scrapeTimeout: 10s
|
|
metricRelabelings:
|
|
- action: labeldrop
|
|
regex: (pod|endpoint|container)
|
|
---
|
|
apiVersion: monitoring.coreos.com/v1
|
|
kind: PrometheusRule
|
|
metadata:
|
|
name: plex
|
|
namespace: {{ .Release.Namespace }}
|
|
labels:
|
|
app.kubernetes.io/name: plex
|
|
spec:
|
|
groups:
|
|
- name: plex.availability
|
|
rules:
|
|
- alert: PlexMetricsTargetDown
|
|
expr: |-
|
|
max(kube_deployment_status_replicas_available{namespace="{{ .Release.Namespace }}",deployment="plex"}) >= 1
|
|
and on()
|
|
(
|
|
max(up{namespace="{{ .Release.Namespace }}",service="plex"}) < 1
|
|
or absent(up{namespace="{{ .Release.Namespace }}",service="plex"})
|
|
)
|
|
for: 10m
|
|
labels:
|
|
severity: warning
|
|
component: exporter
|
|
annotations:
|
|
summary: Plex metrics are unavailable
|
|
description: The Plex Pod is available, but Prometheus cannot scrape its exporter.
|
|
- alert: PlexAPIUnreachable
|
|
expr: max(plex_http_reachable{namespace="{{ .Release.Namespace }}",service="plex"}) < 1
|
|
for: 10m
|
|
labels:
|
|
severity: warning
|
|
component: plex
|
|
annotations:
|
|
summary: Plex API is unreachable from its exporter
|
|
description: The exporter has failed to poll Plex for at least ten minutes.
|
|
- alert: PlexContainerRestarting
|
|
expr: |-
|
|
sum by (container) (
|
|
increase(kube_pod_container_status_restarts_total{namespace="{{ .Release.Namespace }}",pod=~"plex-.*"}[15m])
|
|
) > 2
|
|
for: 5m
|
|
labels:
|
|
severity: warning
|
|
component: kubernetes
|
|
annotations:
|
|
summary: Plex container is repeatedly restarting
|
|
description: Container {{ "{{ $labels.container }}" }} restarted more than twice in 15 minutes.
|
|
- alert: PlexConfigVolumeFilling
|
|
expr: |-
|
|
kubelet_volume_stats_available_bytes{namespace="{{ .Release.Namespace }}",persistentvolumeclaim="{{ default "plex-config" .Values.persistence.existingClaim }}"}
|
|
/
|
|
kubelet_volume_stats_capacity_bytes{namespace="{{ .Release.Namespace }}",persistentvolumeclaim="{{ default "plex-config" .Values.persistence.existingClaim }}"} < 0.15
|
|
for: 30m
|
|
labels:
|
|
severity: warning
|
|
component: storage
|
|
annotations:
|
|
summary: Plex configuration volume is filling
|
|
description: Less than 15 percent of the Plex configuration PVC remains available.
|
|
{{- end }}
|