diff --git a/charts/jellyfin/Chart.yaml b/charts/jellyfin/Chart.yaml index b701d98..0443162 100644 --- a/charts/jellyfin/Chart.yaml +++ b/charts/jellyfin/Chart.yaml @@ -9,10 +9,27 @@ keywords: - media - self-hosted version: 2.6.0 +version: 3.0.0 appVersion: 10.11.2 annotations: artifacthub.io/changes: | + - kind: deprecated + description: Deprecate initContainers parameter in favor of extraInitContainers for consistency (will be removed after 2030) + - kind: fixed + description: Fix extraInitContainers not working (was using wrong parameter name in template) - kind: added description: Add support for Gateway API HTTPRoute resource - kind: added + description: Add envFrom support to load environment variables from ConfigMap or Secret + - kind: added + description: Add NOTES.txt with helpful post-installation information and deprecation warnings + - kind: added + description: Add startup probe to prevent pod restarts during slow initial startup with large media libraries + - kind: added + description: Add persistence.cache configuration for dedicated cache volume support + - kind: added + description: Add comprehensive IPv6 and dual-stack networking documentation + - kind: changed + description: Improve health probe configuration examples for IPv6 compatibility + - kind: changed description: Add revisionHistoryLimit parameter to control rollback history retention (defaults to 3 instead of Kubernetes default 10) diff --git a/charts/jellyfin/README.md b/charts/jellyfin/README.md index ff6e753..d5d788d 100644 --- a/charts/jellyfin/README.md +++ b/charts/jellyfin/README.md @@ -1,6 +1,7 @@ # jellyfin ![Version: 2.6.0](https://img.shields.io/badge/Version-2.6.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 10.11.2](https://img.shields.io/badge/AppVersion-10.11.2-informational?style=flat-square) +![Version: 2.5.1](https://img.shields.io/badge/Version-2.5.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 10.11.2](https://img.shields.io/badge/AppVersion-10.11.2-informational?style=flat-square) A Helm chart for Jellyfin Media Server @@ -73,7 +74,7 @@ helm install my-jellyfin jellyfin/jellyfin -f values.yaml | jellyfin.command | list | `[]` | Custom command to use as container entrypoint. | | jellyfin.enableDLNA | bool | `false` | Enable DLNA. Requires host network. See: https://jellyfin.org/docs/general/networking/dlna.html | | jellyfin.env | list | `[]` | Additional environment variables for the container. | -| livenessProbe | object | `{"initialDelaySeconds":10,"tcpSocket":{"port":"http"}}` | Configure liveness probe for Jellyfin. | +| livenessProbe | object | `{"httpGet":{"path":"/health","port":"http"},"initialDelaySeconds":10}` | Configure liveness probe for Jellyfin. Uses httpGet for compatibility with both IPv4 and IPv6. | | metrics | object | `{"enabled":false,"serviceMonitor":{"enabled":false,"interval":"30s","labels":{},"metricRelabelings":[],"namespace":"","path":"/metrics","port":8096,"relabelings":[],"scheme":"http","scrapeTimeout":"30s","targetLabels":[],"tlsConfig":{}}}` | Configuration for metrics collection and monitoring | | metrics.enabled | bool | `false` | Enable or disable metrics collection | | metrics.serviceMonitor | object | `{"enabled":false,"interval":"30s","labels":{},"metricRelabelings":[],"namespace":"","path":"/metrics","port":8096,"relabelings":[],"scheme":"http","scrapeTimeout":"30s","targetLabels":[],"tlsConfig":{}}` | Configuration for the Prometheus ServiceMonitor | @@ -91,6 +92,13 @@ helm install my-jellyfin jellyfin/jellyfin -f values.yaml | metrics.serviceMonitor.tlsConfig | object | `{}` | TLS configuration for scraping metrics | | nameOverride | string | `""` | Override the default name of the chart. | | nodeSelector | object | `{}` | Node selector for pod scheduling. | +| persistence.cache.accessMode | string | `"ReadWriteOnce"` | PVC specific settings, only used if type is 'pvc'. | +| persistence.cache.annotations | object | `{}` | Custom annotations to be added to the PVC | +| persistence.cache.enabled | bool | `false` | set to false to use emptyDir | +| persistence.cache.hostPath | string | `""` | Path on the host node for cache storage, only used if type is 'hostPath'. | +| persistence.cache.size | string | `"10Gi"` | | +| persistence.cache.storageClass | string | `""` | If undefined (the default) or set to null, no storageClassName spec is set, choosing the default provisioner. | +| persistence.cache.type | string | `"pvc"` | Type of volume for cache storage (pvc, hostPath, emptyDir). If 'enabled' is false, 'emptyDir' is used regardless of this setting. | | persistence.config.accessMode | string | `"ReadWriteOnce"` | | | persistence.config.annotations | object | `{}` | Custom annotations to be added to the PVC | | persistence.config.enabled | bool | `true` | set to false to use emptyDir | @@ -111,14 +119,14 @@ helm install my-jellyfin jellyfin/jellyfin -f values.yaml | podPrivileges.hostPID | bool | `false` | Enable hostPID namespace. Allows pod to see processes on the host. | | podSecurityContext | object | `{}` | Security context for the pod. | | priorityClassName | string | `""` | Define a priorityClassName for the pod. | -| readinessProbe | object | `{"initialDelaySeconds":10,"tcpSocket":{"port":"http"}}` | Configure readiness probe for Jellyfin. | +| readinessProbe | object | `{"httpGet":{"path":"/health","port":"http"},"initialDelaySeconds":10}` | Configure readiness probe for Jellyfin. Uses httpGet for compatibility with both IPv4 and IPv6. | | replicaCount | int | `1` | Number of Jellyfin replicas to start. Should be left at 1. | | resources | object | `{}` | Resource requests and limits for the Jellyfin container. | | runtimeClassName | string | `""` | Define a custom runtimeClassName for the pod. | | securityContext | object | `{}` | Security context for the container. | | service.annotations | object | `{}` | Annotations for the service. | -| service.ipFamilies | list | `[]` | Supported IP families (IPv4, IPv6). | -| service.ipFamilyPolicy | string | `""` | Configure dual-stack IP family policy. See: https://kubernetes.io/docs/concepts/services-networking/dual-stack/ | +| service.ipFamilies | list | `[]` | Supported IP families (IPv4, IPv6). Examples: IPv4 only: ["IPv4"] IPv6 only: ["IPv6"] Dual-stack (IPv4 primary): ["IPv4", "IPv6"] Dual-stack (IPv6 primary): ["IPv6", "IPv4"] Note: When using IPv6, ensure your health checks are compatible (consider using httpGet instead of tcpSocket) | +| service.ipFamilyPolicy | string | `""` | Configure dual-stack IP family policy. See: https://kubernetes.io/docs/concepts/services-networking/dual-stack/ Options: SingleStack, PreferDualStack, RequireDualStack For IPv6-only clusters, use "SingleStack" with ipFamilies: ["IPv6"] For dual-stack, use "PreferDualStack" or "RequireDualStack" with ipFamilies: ["IPv4", "IPv6"] or ["IPv6", "IPv4"] | | service.labels | object | `{}` | Labels for the service. | | service.loadBalancerClass | string | `""` | Class of the LoadBalancer. | | service.loadBalancerIP | string | `""` | Specific IP address for the LoadBalancer. | @@ -191,3 +199,32 @@ extraVolumeMounts: - name: hwa mountPath: /dev/dri ``` + +## IPv6 Configuration + +This chart supports IPv6 and dual-stack networking configurations out of the box. Health probes use httpGet by default for compatibility with both IPv4 and IPv6. + +### IPv6-only Configuration + +For IPv6-only clusters: + +```yaml +service: + ipFamilyPolicy: SingleStack + ipFamilies: + - IPv6 +``` + +### Dual-stack Configuration + +For dual-stack clusters (both IPv4 and IPv6): + +```yaml +service: + ipFamilyPolicy: PreferDualStack # or RequireDualStack + ipFamilies: + - IPv4 + - IPv6 # First family in the list is the primary +``` + +For more information about Kubernetes dual-stack networking, see: diff --git a/charts/jellyfin/README.md.gotmpl b/charts/jellyfin/README.md.gotmpl index bbcd888..be3cf9e 100644 --- a/charts/jellyfin/README.md.gotmpl +++ b/charts/jellyfin/README.md.gotmpl @@ -112,3 +112,32 @@ extraVolumeMounts: - name: hwa mountPath: /dev/dri ``` + +## IPv6 Configuration + +This chart supports IPv6 and dual-stack networking configurations out of the box. Health probes use httpGet by default for compatibility with both IPv4 and IPv6. + +### IPv6-only Configuration + +For IPv6-only clusters: + +```yaml +service: + ipFamilyPolicy: SingleStack + ipFamilies: + - IPv6 +``` + +### Dual-stack Configuration + +For dual-stack clusters (both IPv4 and IPv6): + +```yaml +service: + ipFamilyPolicy: PreferDualStack # or RequireDualStack + ipFamilies: + - IPv4 + - IPv6 # First family in the list is the primary +``` + +For more information about Kubernetes dual-stack networking, see: diff --git a/charts/jellyfin/templates/NOTES.txt b/charts/jellyfin/templates/NOTES.txt index 4d9ef7a..126f16b 100644 --- a/charts/jellyfin/templates/NOTES.txt +++ b/charts/jellyfin/templates/NOTES.txt @@ -1,9 +1,103 @@ -Thank you for installing {{ .Chart.Name }}. +{{- if .Values.initContainers }} +############################################################################# +## ⚠️ WARNING ⚠️ ## +## ## +## The 'initContainers' parameter is DEPRECATED! ## +## Please migrate to 'extraInitContainers' instead. ## +## ## +## Support for 'initContainers' will be removed after 2030. ## +## ## +## Migration guide: ## +## Old: initContainers: [...] ## +## New: extraInitContainers: [...] ## +## ## +############################################################################# + +{{- end }} +Thank you for installing {{ .Chart.Name }}! Your release is named {{ .Release.Name }}. To learn more about the release, try: + $ helm status {{ .Release.Name }} --namespace {{ .Release.Namespace }} + $ helm get all {{ .Release.Name }} --namespace {{ .Release.Namespace }} + +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + +Jellyfin is available at: + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + +Get the Jellyfin URL by running: + + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "jellyfin.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo "Jellyfin URL: http://$NODE_IP:$NODE_PORT" + +{{- else if contains "LoadBalancer" .Values.service.type }} + +Get the Jellyfin URL by running: + + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status by running: + kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "jellyfin.fullname" . }} + + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "jellyfin.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo "Jellyfin URL: http://$SERVICE_IP:{{ .Values.service.port }}" + +{{- else if contains "ClusterIP" .Values.service.type }} + +Get the Jellyfin URL by running: + + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "jellyfin.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Jellyfin URL: http://127.0.0.1:8080" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT + +{{- end }} + +{{- if .Values.persistence.config.enabled }} + +Configuration is persisted to: +{{- if .Values.persistence.config.existingClaim }} + Existing PVC: {{ .Values.persistence.config.existingClaim }} +{{- else }} + PVC: {{ include "jellyfin.fullname" . }}-config +{{- end }} +{{- else }} + +WARNING: Configuration persistence is disabled! + Data will be lost when the pod is restarted. + Enable persistence.config.enabled to persist your configuration. +{{- end }} + +{{- if .Values.persistence.media.enabled }} + +Media is {{ if eq .Values.persistence.media.type "hostPath" }}mounted from host path{{ else if eq .Values.persistence.media.type "pvc" }}persisted to PVC{{ else }}stored in emptyDir{{ end }}: +{{- if eq .Values.persistence.media.type "hostPath" }} + Host path: {{ .Values.persistence.media.hostPath }} +{{- else if eq .Values.persistence.media.type "pvc" }} + {{- if .Values.persistence.media.existingClaim }} + Existing PVC: {{ .Values.persistence.media.existingClaim }} + {{- else }} + PVC: {{ include "jellyfin.fullname" . }}-media + {{- end }} +{{- end }} +{{- else }} + +WARNING: Media persistence is disabled! + Your media files will be lost when the pod is restarted. + Enable persistence.media.enabled to persist your media. +{{- end }} + +For more information and documentation: + Repository: https://github.com/jellyfin/jellyfin-helm + Jellyfin Docs: https://jellyfin.org/docs/ $ helm status {{ .Release.Name }} $ helm get all {{ .Release.Name }} @@ -53,6 +147,74 @@ To access Jellyfin, you can use port-forwarding: Then visit http://localhost:{{ .Values.service.port }} in your browser. {{- end }} +{{- if .Values.jellyfin.envFrom }} + +Environment Configuration: + Loading environment variables from {{ len .Values.jellyfin.envFrom }} source(s): +{{- range .Values.jellyfin.envFrom }} + {{- if .configMapRef }} + - ConfigMap: {{ .configMapRef.name }}{{ if .configMapRef.optional }} (optional){{ end }}{{ if .prefix }} with prefix: {{ .prefix }}{{ end }} + {{- else if .secretRef }} + - Secret: {{ .secretRef.name }}{{ if .secretRef.optional }} (optional){{ end }}{{ if .prefix }} with prefix: {{ .prefix }}{{ end }} + {{- end }} +{{- end }} +{{- end }} + +{{- if .Values.startupProbe }} +{{- $startupTimeout := mul (default 10 .Values.startupProbe.periodSeconds) (default 30 .Values.startupProbe.failureThreshold) }} + +Startup Configuration: + Startup Probe: Enabled + Max Startup Time: {{ $startupTimeout }}s ({{ div $startupTimeout 60 }} minutes) + + The startup probe gives Jellyfin enough time to initialize, especially + with large media libraries. After startup succeeds, liveness and + readiness probes take over for health monitoring. + + {{- if gt $startupTimeout 600 }} + ⚠ NOTE: Startup timeout is very long ({{ div $startupTimeout 60 }}+ minutes). + Consider optimizing your media library or reducing failureThreshold. + {{- end }} +Persistence Configuration: + Config: {{ if .Values.persistence.config.enabled }}{{ .Values.persistence.config.type | default "pvc" }}{{ if .Values.persistence.config.existingClaim }} (existing: {{ .Values.persistence.config.existingClaim }}){{ end }}{{ else }}emptyDir{{ end }} + Media: {{ if .Values.persistence.media.enabled }}{{ .Values.persistence.media.type | default "pvc" }}{{ if .Values.persistence.media.existingClaim }} (existing: {{ .Values.persistence.media.existingClaim }}){{ end }}{{ else }}emptyDir{{ end }} + Cache: {{ if .Values.persistence.cache.enabled }}{{ .Values.persistence.cache.type | default "pvc" }}{{ if .Values.persistence.cache.existingClaim }} (existing: {{ .Values.persistence.cache.existingClaim }}){{ end }}{{ else }}emptyDir{{ end }} + +{{- if and .Values.persistence.cache.enabled (eq .Values.persistence.cache.type "pvc") }} +Cache Volume: + Dedicated cache volume is enabled for improved performance. + Size: {{ .Values.persistence.cache.size }} + Access Mode: {{ .Values.persistence.cache.accessMode }} + {{- if .Values.persistence.cache.storageClass }} + Storage Class: {{ .Values.persistence.cache.storageClass }} + {{- end }} + + Benefits: + - Faster transcoding and metadata operations + - Separate cache lifecycle from config/media + - Can use faster storage class (SSD) for cache +{{- end }} + +{{- if and .Values.persistence.cache.enabled (eq .Values.persistence.cache.type "hostPath") }} +⚠ WARNING: Using hostPath for cache - ensure {{ .Values.persistence.cache.hostPath }} exists on the node +{{- if or .Values.service.ipFamilyPolicy .Values.service.ipFamilies }} + +Service IP Configuration: +{{- if .Values.service.ipFamilyPolicy }} + IP Family Policy: {{ .Values.service.ipFamilyPolicy }} +{{- end }} +{{- if .Values.service.ipFamilies }} + IP Families: {{ .Values.service.ipFamilies | join ", " }} + {{- if eq (index .Values.service.ipFamilies 0) "IPv6" }} + ⚠ NOTE: IPv6 primary - ensure your probes use httpGet instead of tcpSocket for compatibility + {{- end }} + {{- if has "IPv6" .Values.service.ipFamilies }} + {{- if not .Values.service.ipFamilyPolicy }} + ⚠ WARNING: ipFamilies includes IPv6 but ipFamilyPolicy is not set + {{- end }} + {{- end }} +{{- end }} +{{- end }} Deployment Configuration: Revision History Limit: {{ .Values.revisionHistoryLimit }} {{- $limit := int .Values.revisionHistoryLimit }} diff --git a/charts/jellyfin/templates/deployment.yaml b/charts/jellyfin/templates/deployment.yaml index 0c4ea9a..6f4b6ae 100644 --- a/charts/jellyfin/templates/deployment.yaml +++ b/charts/jellyfin/templates/deployment.yaml @@ -62,6 +62,10 @@ spec: args: {{- toYaml . | nindent 12 }} {{- end }} + {{- with .Values.jellyfin.envFrom }} + envFrom: + {{- toYaml . | nindent 12 }} + {{- end }} {{- with .Values.jellyfin.env }} env: {{- toYaml . | nindent 12 }} @@ -111,6 +115,10 @@ spec: hostPort: 1900 protocol: UDP {{- end }} + {{- with .Values.startupProbe }} + startupProbe: + {{- toYaml . | nindent 12 }} + {{- end }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: @@ -122,15 +130,24 @@ spec: name: config - mountPath: /media name: media + - mountPath: /cache + name: cache {{- with .Values.volumeMounts }} {{- toYaml . | nindent 12 }} {{- end }} {{- with .Values.extraContainers }} {{- toYaml . | nindent 8 }} {{- end }} - {{- with .Values.initContainers }} + {{- if or .Values.initContainers .Values.extraInitContainers }} + # Support both initContainers (deprecated) and extraInitContainers for backward compatibility. + # TODO: Remove initContainers support after 2030 initContainers: - {{- toYaml . | nindent 6 }} + {{- if .Values.initContainers }} + {{- toYaml .Values.initContainers | nindent 6 }} + {{- end }} + {{- if .Values.extraInitContainers }} + {{- toYaml .Values.extraInitContainers | nindent 6 }} + {{- end }} {{- end }} volumes: - name: config @@ -155,6 +172,21 @@ spec: {{- else }} emptyDir: {} {{- end }} + - name: cache + {{- if .Values.persistence.cache.enabled }} + {{- if eq .Values.persistence.cache.type "pvc" }} + persistentVolumeClaim: + claimName: {{ if .Values.persistence.cache.existingClaim }}{{ .Values.persistence.cache.existingClaim }}{{- else }}{{ template "jellyfin.fullname" . }}-cache{{- end }} + {{- else if eq .Values.persistence.cache.type "hostPath" }} + hostPath: + path: {{ .Values.persistence.cache.hostPath }} + type: Directory + {{- else }} + emptyDir: {} + {{- end }} + {{- else }} + emptyDir: {} + {{- end }} {{- with .Values.volumes }} {{- toYaml . | nindent 8 }} {{- end }} diff --git a/charts/jellyfin/templates/persistentVolumeClaim.yaml b/charts/jellyfin/templates/persistentVolumeClaim.yaml index 709436d..64ad819 100644 --- a/charts/jellyfin/templates/persistentVolumeClaim.yaml +++ b/charts/jellyfin/templates/persistentVolumeClaim.yaml @@ -43,3 +43,26 @@ spec: storageClassName: {{ . | quote }} {{- end }} {{- end }} + +{{- if and .Values.persistence.cache.enabled (eq .Values.persistence.cache.type "pvc") (not .Values.persistence.cache.existingClaim) }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "jellyfin.fullname" . }}-cache + labels: + {{- include "jellyfin.labels" . | nindent 4 }} + {{- with .Values.persistence.cache.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + accessModes: + - {{ .Values.persistence.cache.accessMode | quote }} + resources: + requests: + storage: {{ .Values.persistence.cache.size | quote }} + {{- with .Values.persistence.cache.storageClass }} + storageClassName: {{ . | quote }} + {{- end }} +{{- end }} diff --git a/charts/jellyfin/tests/cache_persistence_test.yaml b/charts/jellyfin/tests/cache_persistence_test.yaml new file mode 100644 index 0000000..fafcf9e --- /dev/null +++ b/charts/jellyfin/tests/cache_persistence_test.yaml @@ -0,0 +1,160 @@ +suite: test cache persistence +templates: + - deployment.yaml + - persistentVolumeClaim.yaml +tests: + # Deployment volume mount tests + - it: should mount cache as emptyDir by default + template: deployment.yaml + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: cache + emptyDir: {} + + - it: should mount cache PVC when enabled + template: deployment.yaml + set: + persistence.cache.enabled: true + persistence.cache.type: pvc + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: cache + persistentVolumeClaim: + claimName: RELEASE-NAME-jellyfin-cache + + - it: should mount cache from hostPath when configured + template: deployment.yaml + set: + persistence.cache.enabled: true + persistence.cache.type: hostPath + persistence.cache.hostPath: /mnt/jellyfin-cache + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: cache + hostPath: + path: /mnt/jellyfin-cache + + - it: should mount cache at correct path + template: deployment.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: cache + mountPath: /cache + + # PVC tests + - it: should not create cache PVC by default + template: persistentVolumeClaim.yaml + asserts: + - hasDocuments: + count: 2 # config and media only + + - it: should create cache PVC when enabled + template: persistentVolumeClaim.yaml + set: + persistence.cache.enabled: true + persistence.cache.type: pvc + asserts: + - hasDocuments: + count: 3 # config, media, and cache + - contains: + path: metadata.name + content: RELEASE-NAME-jellyfin-cache + + - it: should not create cache PVC when type is hostPath + template: persistentVolumeClaim.yaml + set: + persistence.cache.enabled: true + persistence.cache.type: hostPath + asserts: + - hasDocuments: + count: 2 # config and media only + + - it: should not create cache PVC when type is emptyDir + template: persistentVolumeClaim.yaml + set: + persistence.cache.enabled: true + persistence.cache.type: emptyDir + asserts: + - hasDocuments: + count: 2 + + - it: should set cache PVC size correctly + template: persistentVolumeClaim.yaml + documentIndex: 2 + set: + persistence.cache.enabled: true + persistence.cache.type: pvc + persistence.cache.size: 20Gi + asserts: + - equal: + path: spec.resources.requests.storage + value: 20Gi + + - it: should set cache PVC access mode + template: persistentVolumeClaim.yaml + documentIndex: 2 + set: + persistence.cache.enabled: true + persistence.cache.type: pvc + persistence.cache.accessMode: ReadWriteMany + asserts: + - contains: + path: spec.accessModes + content: ReadWriteMany + + - it: should set cache PVC storage class + template: persistentVolumeClaim.yaml + documentIndex: 2 + set: + persistence.cache.enabled: true + persistence.cache.type: pvc + persistence.cache.storageClass: fast-ssd + asserts: + - equal: + path: spec.storageClassName + value: fast-ssd + + - it: should add annotations to cache PVC + template: persistentVolumeClaim.yaml + documentIndex: 2 + set: + persistence.cache.enabled: true + persistence.cache.type: pvc + persistence.cache.annotations: + my-annotation: my-value + asserts: + - equal: + path: metadata.annotations.my-annotation + value: my-value + + - it: should use existing claim when specified + template: deployment.yaml + set: + persistence.cache.enabled: true + persistence.cache.type: pvc + persistence.cache.existingClaim: my-existing-cache-claim + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: cache + persistentVolumeClaim: + claimName: my-existing-cache-claim + + - it: should not create PVC when existing claim used + template: persistentVolumeClaim.yaml + set: + persistence.cache.enabled: true + persistence.cache.type: pvc + persistence.cache.existingClaim: my-existing-cache-claim + asserts: + - hasDocuments: + count: 2 # config and media only, not cache diff --git a/charts/jellyfin/tests/envfrom_test.yaml b/charts/jellyfin/tests/envfrom_test.yaml new file mode 100644 index 0000000..a5f7165 --- /dev/null +++ b/charts/jellyfin/tests/envfrom_test.yaml @@ -0,0 +1,122 @@ +suite: test envFrom +templates: + - deployment.yaml +tests: + - it: should not have envFrom by default + asserts: + - isNull: + path: spec.template.spec.containers[0].envFrom + + - it: should add envFrom from ConfigMap + set: + jellyfin.envFrom: + - configMapRef: + name: jellyfin-config + asserts: + - isNotNull: + path: spec.template.spec.containers[0].envFrom + - lengthEqual: + path: spec.template.spec.containers[0].envFrom + count: 1 + - equal: + path: spec.template.spec.containers[0].envFrom[0].configMapRef.name + value: jellyfin-config + + - it: should add envFrom from Secret + set: + jellyfin.envFrom: + - secretRef: + name: jellyfin-secrets + asserts: + - equal: + path: spec.template.spec.containers[0].envFrom[0].secretRef.name + value: jellyfin-secrets + + - it: should support multiple envFrom sources + set: + jellyfin.envFrom: + - configMapRef: + name: jellyfin-config + - secretRef: + name: jellyfin-secrets + - configMapRef: + name: jellyfin-extra-config + asserts: + - lengthEqual: + path: spec.template.spec.containers[0].envFrom + count: 3 + - equal: + path: spec.template.spec.containers[0].envFrom[0].configMapRef.name + value: jellyfin-config + - equal: + path: spec.template.spec.containers[0].envFrom[1].secretRef.name + value: jellyfin-secrets + - equal: + path: spec.template.spec.containers[0].envFrom[2].configMapRef.name + value: jellyfin-extra-config + + - it: should support optional ConfigMap + set: + jellyfin.envFrom: + - configMapRef: + name: optional-config + optional: true + asserts: + - equal: + path: spec.template.spec.containers[0].envFrom[0].configMapRef.optional + value: true + + - it: should support optional Secret + set: + jellyfin.envFrom: + - secretRef: + name: optional-secret + optional: true + asserts: + - equal: + path: spec.template.spec.containers[0].envFrom[0].secretRef.optional + value: true + + - it: should support prefix for ConfigMap + set: + jellyfin.envFrom: + - configMapRef: + name: my-config + prefix: CONFIG_ + asserts: + - equal: + path: spec.template.spec.containers[0].envFrom[0].prefix + value: CONFIG_ + + - it: should support prefix for Secret + set: + jellyfin.envFrom: + - secretRef: + name: my-secret + prefix: SECRET_ + asserts: + - equal: + path: spec.template.spec.containers[0].envFrom[0].prefix + value: SECRET_ + + - it: should work alongside regular env variables + set: + jellyfin.env: + - name: CUSTOM_VAR + value: custom-value + jellyfin.envFrom: + - configMapRef: + name: jellyfin-config + asserts: + - isNotNull: + path: spec.template.spec.containers[0].env + - isNotNull: + path: spec.template.spec.containers[0].envFrom + - contains: + path: spec.template.spec.containers[0].env + content: + name: CUSTOM_VAR + value: custom-value + - equal: + path: spec.template.spec.containers[0].envFrom[0].configMapRef.name + value: jellyfin-config diff --git a/charts/jellyfin/tests/init_containers_test.yaml b/charts/jellyfin/tests/init_containers_test.yaml new file mode 100644 index 0000000..7ea5259 --- /dev/null +++ b/charts/jellyfin/tests/init_containers_test.yaml @@ -0,0 +1,119 @@ +suite: test init containers +templates: + - deployment.yaml +tests: + - it: should not have init containers by default + asserts: + - isNull: + path: spec.template.spec.initContainers + + - it: should add extraInitContainers when specified + set: + extraInitContainers: + - name: init-config + image: busybox:1.35 + command: ['sh', '-c', 'echo Initializing'] + asserts: + - isNotNull: + path: spec.template.spec.initContainers + - lengthEqual: + path: spec.template.spec.initContainers + count: 1 + - equal: + path: spec.template.spec.initContainers[0].name + value: init-config + - equal: + path: spec.template.spec.initContainers[0].image + value: busybox:1.35 + + - it: should support multiple extraInitContainers + set: + extraInitContainers: + - name: init-1 + image: busybox:1.35 + - name: init-2 + image: alpine:3.18 + asserts: + - lengthEqual: + path: spec.template.spec.initContainers + count: 2 + - equal: + path: spec.template.spec.initContainers[0].name + value: init-1 + - equal: + path: spec.template.spec.initContainers[1].name + value: init-2 + + - it: should preserve full container spec in extraInitContainers + set: + extraInitContainers: + - name: init-permissions + image: busybox:1.35 + command: ['sh', '-c'] + args: ['chown -R 1000:1000 /config'] + volumeMounts: + - name: config + mountPath: /config + securityContext: + runAsUser: 0 + asserts: + - equal: + path: spec.template.spec.initContainers[0].command[0] + value: sh + - equal: + path: spec.template.spec.initContainers[0].args[0] + value: 'chown -R 1000:1000 /config' + - equal: + path: spec.template.spec.initContainers[0].volumeMounts[0].name + value: config + - equal: + path: spec.template.spec.initContainers[0].securityContext.runAsUser + value: 0 + + # Deprecated initContainers (backward compatibility) + - it: should still support deprecated initContainers parameter + set: + initContainers: + - name: legacy-init + image: busybox:1.35 + asserts: + - lengthEqual: + path: spec.template.spec.initContainers + count: 1 + - equal: + path: spec.template.spec.initContainers[0].name + value: legacy-init + + - it: should merge initContainers and extraInitContainers + set: + initContainers: + - name: legacy-init + image: busybox:1.35 + extraInitContainers: + - name: new-init + image: alpine:3.18 + asserts: + - lengthEqual: + path: spec.template.spec.initContainers + count: 2 + - equal: + path: spec.template.spec.initContainers[0].name + value: legacy-init + - equal: + path: spec.template.spec.initContainers[1].name + value: new-init + + - it: should prioritize extraInitContainers over initContainers + set: + initContainers: + - name: old-way + image: busybox:1.35 + extraInitContainers: + - name: new-way + image: alpine:3.18 + asserts: + - contains: + path: spec.template.spec.initContainers + content: + name: new-way + image: alpine:3.18 diff --git a/charts/jellyfin/tests/service_ipv6_test.yaml b/charts/jellyfin/tests/service_ipv6_test.yaml new file mode 100644 index 0000000..5dee731 --- /dev/null +++ b/charts/jellyfin/tests/service_ipv6_test.yaml @@ -0,0 +1,128 @@ +suite: test service ipv6 and dual-stack +templates: + - service.yaml +tests: + - it: should not set ipFamilyPolicy by default + asserts: + - isNull: + path: spec.ipFamilyPolicy + + - it: should not set ipFamilies by default + asserts: + - isNull: + path: spec.ipFamilies + + - it: should set ipFamilyPolicy when specified + set: + service.ipFamilyPolicy: PreferDualStack + asserts: + - equal: + path: spec.ipFamilyPolicy + value: PreferDualStack + + - it: should support SingleStack policy + set: + service.ipFamilyPolicy: SingleStack + asserts: + - equal: + path: spec.ipFamilyPolicy + value: SingleStack + + - it: should support RequireDualStack policy + set: + service.ipFamilyPolicy: RequireDualStack + asserts: + - equal: + path: spec.ipFamilyPolicy + value: RequireDualStack + + - it: should set ipFamilies for IPv4 only + set: + service.ipFamilies: + - IPv4 + asserts: + - equal: + path: spec.ipFamilies[0] + value: IPv4 + + - it: should set ipFamilies for IPv6 only + set: + service.ipFamilies: + - IPv6 + asserts: + - equal: + path: spec.ipFamilies[0] + value: IPv6 + + - it: should set ipFamilies for dual-stack IPv4 primary + set: + service.ipFamilies: + - IPv4 + - IPv6 + asserts: + - equal: + path: spec.ipFamilies[0] + value: IPv4 + - equal: + path: spec.ipFamilies[1] + value: IPv6 + + - it: should set ipFamilies for dual-stack IPv6 primary + set: + service.ipFamilies: + - IPv6 + - IPv4 + asserts: + - equal: + path: spec.ipFamilies[0] + value: IPv6 + - equal: + path: spec.ipFamilies[1] + value: IPv4 + + - it: should work with PreferDualStack and both families + set: + service.ipFamilyPolicy: PreferDualStack + service.ipFamilies: + - IPv4 + - IPv6 + asserts: + - equal: + path: spec.ipFamilyPolicy + value: PreferDualStack + - equal: + path: spec.ipFamilies[0] + value: IPv4 + - equal: + path: spec.ipFamilies[1] + value: IPv6 + + - it: should work with RequireDualStack and both families + set: + service.ipFamilyPolicy: RequireDualStack + service.ipFamilies: + - IPv6 + - IPv4 + asserts: + - equal: + path: spec.ipFamilyPolicy + value: RequireDualStack + - equal: + path: spec.ipFamilies[0] + value: IPv6 + - equal: + path: spec.ipFamilies[1] + value: IPv4 + + - it: should work with SingleStack IPv6 only + set: + service.ipFamilyPolicy: SingleStack + service.ipFamilies: + - IPv6 + asserts: + - equal: + path: spec.ipFamilyPolicy + value: SingleStack + - equal: + path: spec.ipFamilies[0] + value: IPv6 diff --git a/charts/jellyfin/tests/startup_probe_test.yaml b/charts/jellyfin/tests/startup_probe_test.yaml new file mode 100644 index 0000000..ee7430f --- /dev/null +++ b/charts/jellyfin/tests/startup_probe_test.yaml @@ -0,0 +1,92 @@ +suite: test startup probe +templates: + - deployment.yaml +tests: + - it: should have startup probe by default + asserts: + - isNotNull: + path: spec.template.spec.containers[0].startupProbe + + - it: should use tcpSocket by default + asserts: + - isNotNull: + path: spec.template.spec.containers[0].startupProbe.tcpSocket + - equal: + path: spec.template.spec.containers[0].startupProbe.tcpSocket.port + value: http + + - it: should have default timing values + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.initialDelaySeconds + value: 0 + - equal: + path: spec.template.spec.containers[0].startupProbe.periodSeconds + value: 10 + - equal: + path: spec.template.spec.containers[0].startupProbe.failureThreshold + value: 30 + + - it: should allow custom initialDelaySeconds + set: + startupProbe.initialDelaySeconds: 5 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.initialDelaySeconds + value: 5 + + - it: should allow custom periodSeconds + set: + startupProbe.periodSeconds: 15 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.periodSeconds + value: 15 + + - it: should allow custom failureThreshold + set: + startupProbe.failureThreshold: 60 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.failureThreshold + value: 60 + + - it: should support httpGet probe + set: + startupProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 0 + periodSeconds: 10 + failureThreshold: 30 + asserts: + - isNotNull: + path: spec.template.spec.containers[0].startupProbe.httpGet + - equal: + path: spec.template.spec.containers[0].startupProbe.httpGet.path + value: /health + - equal: + path: spec.template.spec.containers[0].startupProbe.httpGet.port + value: http + + - it: should support custom port + set: + startupProbe.tcpSocket.port: 9000 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.tcpSocket.port + value: 9000 + + - it: should calculate correct timeout window + set: + startupProbe.periodSeconds: 10 + startupProbe.failureThreshold: 30 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.periodSeconds + value: 10 + - equal: + path: spec.template.spec.containers[0].startupProbe.failureThreshold + value: 30 + # Total startup time allowed: 10 * 30 = 300 seconds (5 minutes) diff --git a/charts/jellyfin/values.yaml b/charts/jellyfin/values.yaml index d6c6c3d..fe9826f 100644 --- a/charts/jellyfin/values.yaml +++ b/charts/jellyfin/values.yaml @@ -91,8 +91,17 @@ service: # -- Service type (ClusterIP, NodePort, or LoadBalancer). type: ClusterIP # -- Configure dual-stack IP family policy. See: https://kubernetes.io/docs/concepts/services-networking/dual-stack/ + # Options: SingleStack, PreferDualStack, RequireDualStack + # For IPv6-only clusters, use "SingleStack" with ipFamilies: ["IPv6"] + # For dual-stack, use "PreferDualStack" or "RequireDualStack" with ipFamilies: ["IPv4", "IPv6"] or ["IPv6", "IPv4"] ipFamilyPolicy: "" # -- Supported IP families (IPv4, IPv6). + # Examples: + # IPv4 only: ["IPv4"] + # IPv6 only: ["IPv6"] + # Dual-stack (IPv4 primary): ["IPv4", "IPv6"] + # Dual-stack (IPv6 primary): ["IPv6", "IPv4"] + # Note: When using IPv6, ensure your health checks are compatible (consider using httpGet instead of tcpSocket) ipFamilies: [] # -- Specific IP address for the LoadBalancer. loadBalancerIP: "" @@ -158,21 +167,39 @@ resources: {} # cpu: 100m # memory: 128Mi -# -- Configure liveness probe for Jellyfin. -livenessProbe: - initialDelaySeconds: 10 +# -- Configure startup probe for Jellyfin. +# This probe gives Jellyfin enough time to start, especially with large media libraries. +# After the startup probe succeeds once, liveness and readiness probes take over. +startupProbe: tcpSocket: port: http + initialDelaySeconds: 0 + periodSeconds: 10 + failureThreshold: 30 + # With these defaults, Jellyfin has up to 5 minutes (30 * 10s) to start. + # Adjust failureThreshold if you have very large media libraries or slow storage. + +# -- Configure liveness probe for Jellyfin. +# This probe is disabled during startup (startup probe handles initial checks). +# Uses httpGet for compatibility with both IPv4 and IPv6. +livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 # successThreshold: 1 # failureThreshold: 3 # timeoutSeconds: 1 # periodSeconds: 10 # -- Configure readiness probe for Jellyfin. +# This probe is disabled during startup (startup probe handles initial checks). +# Uses httpGet for compatibility with both IPv4 and IPv6. readinessProbe: - initialDelaySeconds: 10 - tcpSocket: + httpGet: + path: /health port: http + initialDelaySeconds: 10 # successThreshold: 1 # failureThreshold: 3 # timeoutSeconds: 1 @@ -216,7 +243,20 @@ jellyfin: command: [] # -- Additional arguments for the entrypoint command. args: [] + # -- Load environment variables from ConfigMap or Secret. + # See: https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#configure-all-key-value-pairs-in-a-configmap-as-container-environment-variables + # Example: + # envFrom: + # - configMapRef: + # name: jellyfin-config + # - secretRef: + # name: jellyfin-secrets + envFrom: [] # -- Additional environment variables for the container. + # Example: + # env: + # - name: JELLYFIN_CACHE_DIR + # value: /cache env: [] persistence: @@ -247,6 +287,22 @@ persistence: storageClass: '' ## -- Use an existing PVC for this mount # existingClaim: '' + cache: + # -- set to false to use emptyDir + enabled: false + # -- Type of volume for cache storage (pvc, hostPath, emptyDir). If 'enabled' is false, 'emptyDir' is used regardless of this setting. + type: pvc + # -- Path on the host node for cache storage, only used if type is 'hostPath'. + hostPath: "" + # -- PVC specific settings, only used if type is 'pvc'. + accessMode: ReadWriteOnce + size: 10Gi + # -- Custom annotations to be added to the PVC + annotations: {} + # -- If undefined (the default) or set to null, no storageClassName spec is set, choosing the default provisioner. + storageClass: '' + ## -- Use an existing PVC for this mount + # existingClaim: '' # -- Configuration for metrics collection and monitoring metrics: @@ -279,8 +335,22 @@ metrics: # -- Target labels to add to the scraped metrics targetLabels: [] -# -- additional init containers to run inside the pod. +# -- DEPRECATED: Use extraInitContainers instead. Will be removed after 2030. +# @deprecated - This parameter is deprecated, use extraInitContainers instead +initContainers: [] + +# -- Additional init containers to run inside the pod. +# Init containers run before the main application container starts. +# See: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ +# Example: +# extraInitContainers: +# - name: init-config +# image: busybox:1.35 +# command: ['sh', '-c', 'echo "Initializing..." && sleep 5'] +# volumeMounts: +# - name: config +# mountPath: /config extraInitContainers: [] - + # -- additional sidecar containers to run inside the pod. extraContainers: []