diff --git a/.github/workflows/pr-checks.yaml b/.github/workflows/pr-checks.yaml new file mode 100644 index 0000000..dfceb6b --- /dev/null +++ b/.github/workflows/pr-checks.yaml @@ -0,0 +1,93 @@ +name: PR Checks + +on: + pull_request: + paths: + - 'charts/**' + - '.github/workflows/pr-checks.yaml' + - 'ct.yaml' + +permissions: + contents: read + pull-requests: read + +jobs: + lint-test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.14.0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + # Check 1: Verify helm-docs is up to date + - name: Verify helm-docs up to date + uses: losisin/helm-docs-github-action@v1 + with: + fail-on-diff: true + + # Check 2: Run helm-unittest tests + - name: Install helm-unittest plugin + run: helm plugin install https://github.com/helm-unittest/helm-unittest.git + + - name: Run helm-unittest + run: | + helm unittest charts/jellyfin --with-subchart=false + + # Check 3: Helm lint (includes schema validation) + - name: Helm lint + run: helm lint charts/jellyfin + + # Check 4: Helm template validation + - name: Helm template validation + run: helm template test charts/jellyfin --debug > /dev/null + + # Check 5: Chart-testing (ct lint) + - name: Set up chart-testing + uses: helm/chart-testing-action@v2 + + - name: Run chart-testing (list-changed) + id: list-changed + run: | + changed=$(ct list-changed --target-branch master --config ct.yaml) + if [[ -n "$changed" ]]; then + echo "changed=true" >> $GITHUB_OUTPUT + fi + + - name: Run chart-testing (lint) + if: steps.list-changed.outputs.changed == 'true' + run: ct lint --config ct.yaml + + # Check 6: Kubernetes manifest validation with kubeconform + - name: Install kubeconform + run: | + # renovate: datasource=github-releases depName=yannh/kubeconform + VERSION=v0.6.7 + wget -qO- https://github.com/yannh/kubeconform/releases/download/${VERSION}/kubeconform-linux-amd64.tar.gz | tar xz + sudo mv kubeconform /usr/local/bin/ + kubeconform -v + + - name: Validate Kubernetes manifests + run: | + # Validate against multiple Kubernetes versions (last 5 stable releases) + for k8s_version in 1.30.0 1.31.0 1.32.0 1.33.0 1.34.0; do + echo "Validating against Kubernetes $k8s_version" + helm template test charts/jellyfin \ + | kubeconform -strict -summary -kubernetes-version $k8s_version + done + + # Check 7: Markdown lint + - name: Lint markdown files + uses: DavidAnson/markdownlint-cli2-action@v18 + with: + globs: 'charts/**/*.md' diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000..15d1f8c --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,25 @@ +# markdownlint configuration +# https://github.com/DavidAnson/markdownlint + +default: true + +# MD013/line-length - Line length (disabled for technical docs) +MD013: false + +# MD022/blanks-around-headings - Disabled for helm-docs generated content +MD022: false + +# MD031/blanks-around-fences - Disabled for helm-docs generated content +MD031: false + +# MD032/blanks-around-lists - Disabled for helm-docs generated content +MD032: false + +# MD033/no-inline-html +MD033: false + +# MD034/no-bare-urls - Disabled for helm-docs generated content +MD034: false + +# MD041/first-line-heading +MD041: false diff --git a/.yamllint.yaml b/.yamllint.yaml new file mode 100644 index 0000000..4635faf --- /dev/null +++ b/.yamllint.yaml @@ -0,0 +1,21 @@ +# yamllint configuration +# https://yamllint.readthedocs.io/ + +extends: default + +ignore: | + charts/*/templates/ + .github/ + +rules: + line-length: + max: 120 + level: warning + document-start: disable + truthy: + allowed-values: ['true', 'false', 'on', 'off'] + comments: + min-spaces-from-content: 1 + indentation: + spaces: 2 + indent-sequences: true diff --git a/charts/jellyfin/Chart.yaml b/charts/jellyfin/Chart.yaml index 2bb4304..d73efbe 100644 --- a/charts/jellyfin/Chart.yaml +++ b/charts/jellyfin/Chart.yaml @@ -9,8 +9,23 @@ keywords: - media - self-hosted version: 3.0.0 +maintainers: + - name: Jellyfin Project + url: https://jellyfin.org appVersion: "10.11.5" annotations: + artifacthub.io/links: | + - name: Documentation + url: https://jellyfin.org/docs/ + - name: GitHub Repository + url: https://github.com/jellyfin/jellyfin + - name: Helm Chart Repository + url: https://github.com/jellyfin/jellyfin-helm + - name: Support + url: https://jellyfin.org/contact/ + artifacthub.io/maintainers: | + - name: Jellyfin Project + url: https://jellyfin.org artifacthub.io/changes: | - kind: changed description: Update Jellyfin to 10.11.5 @@ -23,6 +38,7 @@ annotations: links: - name: Documentation url: https://kubernetes.io/docs/concepts/services-networking/network-policies/ + - kind: added description: Add troubleshooting documentation for inotify instance limits with workaround example - kind: added description: Add support for Gateway API HTTPRoute resource diff --git a/charts/jellyfin/LICENSE b/charts/jellyfin/LICENSE new file mode 100644 index 0000000..4522ba0 --- /dev/null +++ b/charts/jellyfin/LICENSE @@ -0,0 +1,339 @@ +GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {{description}} + Copyright (C) {{year}} {{fullname}} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/charts/jellyfin/MIGRATION.md b/charts/jellyfin/MIGRATION.md index d49825a..9f3d29c 100644 --- a/charts/jellyfin/MIGRATION.md +++ b/charts/jellyfin/MIGRATION.md @@ -10,9 +10,10 @@ extraPodLabels -> podLabels extraPodAnnotations -> podAnnotations ``` -3. The `extraEnvVars` key has been moved and renamed to `jellyfin.env`. -4. `extraVolumes` has been moved to `volumes`, and `extraVolumeMounts` has been moved to `volumeMounts`. -5. The `extraExistingClaimMounts` key has been removed, as it can now be represented with `volumes` and `volumeMounts`. + +1. The `extraEnvVars` key has been moved and renamed to `jellyfin.env`. +2. `extraVolumes` has been moved to `volumes`, and `extraVolumeMounts` has been moved to `volumeMounts`. +3. The `extraExistingClaimMounts` key has been removed, as it can now be represented with `volumes` and `volumeMounts`. ### Service @@ -41,4 +42,4 @@ PVC creation is now enabled by default to prevent data loss when the chart is us ### Probes -The liveness and readiness probes are now always enabled to ensure proper Kubernetes lifecycle management. Adjust the values accordingly if you have a large library. \ No newline at end of file +The liveness and readiness probes are now always enabled to ensure proper Kubernetes lifecycle management. Adjust the values accordingly if you have a large library. diff --git a/charts/jellyfin/README.md b/charts/jellyfin/README.md index 8b612bf..c5b7c13 100644 --- a/charts/jellyfin/README.md +++ b/charts/jellyfin/README.md @@ -6,47 +6,31 @@ A Helm chart for Jellyfin Media Server **Homepage:** -## Steps to Use a Helm Chart - -### 1. Add a Helm Repository - -Helm repositories contain collections of charts. You can add an existing repository using the following command: +## Quick Start ```bash +# Add the Jellyfin Helm repository helm repo add jellyfin https://jellyfin.github.io/jellyfin-helm -``` - -### 2. Install the Helm Chart +helm repo update -To install a chart, use the following command: - -```bash +# Install with default settings (ephemeral storage) helm install my-jellyfin jellyfin/jellyfin -``` - -### 3. View the Installation -You can check the status of the release using: - -```bash -helm status my-jellyfin +# Install with persistent storage +helm install my-jellyfin jellyfin/jellyfin \ + --set persistence.config.enabled=true \ + --set persistence.media.enabled=true ``` -## Customizing the Chart +For production deployments, create a custom `values.yaml` file with your configuration and install using: -Helm charts come with default values, but you can customize them by using the --set flag or by providing a custom values.yaml file. - -### 1. Using --set to Override Values ```bash -helm install my-jellyfin jellyfin/jellyfin --set key1=value1,key2=value2 +helm install my-jellyfin jellyfin/jellyfin --values values.yaml ``` -### 2. Using a values.yaml File -You can create a custom values.yaml file and pass it to the install command: +## Maintainers -```bash -helm install my-jellyfin jellyfin/jellyfin -f values.yaml -``` +This chart is maintained by the [Jellyfin Project](https://jellyfin.org). ## Values @@ -403,7 +387,7 @@ networkPolicy: 4. **Testing**: Always test NetworkPolicy changes in a development environment first. Misconfigured policies can block legitimate traffic. -### Troubleshooting +### NetworkPolicy Troubleshooting **Jellyfin can't download metadata/images:** - Check that `egress.allowAllEgress: true` or `restrictedEgress.allowMetadata: true` is set @@ -429,7 +413,8 @@ For more configuration options, see the full values documentation in [values.yam ### inotify Instance Limit Reached **Problem:** Jellyfin crashes with error: -``` + +```text System.IO.IOException: The configured user limit (128) on the number of inotify instances has been reached ``` diff --git a/charts/jellyfin/README.md.gotmpl b/charts/jellyfin/README.md.gotmpl index fa63b7c..3c6ba46 100644 --- a/charts/jellyfin/README.md.gotmpl +++ b/charts/jellyfin/README.md.gotmpl @@ -7,49 +7,31 @@ {{ template "chart.homepageLine" . }} -## Steps to Use a Helm Chart - -### 1. Add a Helm Repository - -Helm repositories contain collections of charts. You can add an existing repository using the following command: +## Quick Start ```bash +# Add the Jellyfin Helm repository helm repo add jellyfin https://jellyfin.github.io/jellyfin-helm -``` - -### 2. Install the Helm Chart +helm repo update -To install a chart, use the following command: - -```bash +# Install with default settings (ephemeral storage) helm install my-jellyfin jellyfin/jellyfin -``` - -### 3. View the Installation -You can check the status of the release using: - -```bash -helm status my-jellyfin +# Install with persistent storage +helm install my-jellyfin jellyfin/jellyfin \ + --set persistence.config.enabled=true \ + --set persistence.media.enabled=true ``` -## Customizing the Chart +For production deployments, create a custom `values.yaml` file with your configuration and install using: -Helm charts come with default values, but you can customize them by using the --set flag or by providing a custom values.yaml file. - -### 1. Using --set to Override Values ```bash -helm install my-jellyfin jellyfin/jellyfin --set key1=value1,key2=value2 +helm install my-jellyfin jellyfin/jellyfin --values values.yaml ``` -### 2. Using a values.yaml File -You can create a custom values.yaml file and pass it to the install command: - -```bash -helm install my-jellyfin jellyfin/jellyfin -f values.yaml -``` +## Maintainers -{{ template "chart.maintainersSection" . }} +This chart is maintained by the [Jellyfin Project](https://jellyfin.org). {{ template "chart.sourcesSection" . }} @@ -292,7 +274,7 @@ networkPolicy: 4. **Testing**: Always test NetworkPolicy changes in a development environment first. Misconfigured policies can block legitimate traffic. -### Troubleshooting +### NetworkPolicy Troubleshooting **Jellyfin can't download metadata/images:** - Check that `egress.allowAllEgress: true` or `restrictedEgress.allowMetadata: true` is set @@ -318,7 +300,8 @@ For more configuration options, see the full values documentation in [values.yam ### inotify Instance Limit Reached **Problem:** Jellyfin crashes with error: -``` + +```text System.IO.IOException: The configured user limit (128) on the number of inotify instances has been reached ``` diff --git a/charts/jellyfin/examples/README.md b/charts/jellyfin/examples/README.md new file mode 100644 index 0000000..27eef2f --- /dev/null +++ b/charts/jellyfin/examples/README.md @@ -0,0 +1,128 @@ +# Jellyfin Helm Chart Examples + +This directory contains example configurations for common Jellyfin deployment scenarios. + +## Available Examples + +### 1. Minimal (`minimal.yaml`) + +Quick start configuration with ephemeral storage for testing purposes. + +```bash +helm install jellyfin jellyfin/jellyfin -f examples/minimal.yaml +``` + +**Use case**: Development, testing, demo environments. + +**Note**: All data will be lost when the pod is restarted. + +--- + +### 2. Production (`production.yaml`) + +Production-ready configuration with persistent storage, resource limits, and optimized health probes. + +```bash +helm install jellyfin jellyfin/jellyfin -f examples/production.yaml +``` + +**Features**: + +- Persistent storage for config, media, and cache +- Resource requests and limits +- Optimized health probes for large libraries +- Security context with non-root user + +--- + +### 3. Hardware Acceleration (`hardware-acceleration.yaml`) + +Enable GPU acceleration for video transcoding (Intel QuickSync example). + +```bash +helm install jellyfin jellyfin/jellyfin -f examples/hardware-acceleration.yaml +``` + +**Prerequisites**: + +- Node with compatible GPU (Intel/AMD/NVIDIA) +- GPU device plugin installed in cluster +- Appropriate node labels/taints + +**Note**: Adjust device paths and capabilities for your specific GPU type. See [Jellyfin documentation](https://jellyfin.org/docs/general/administration/hardware-acceleration/) for details. + +--- + +### 4. Ingress with TLS (`ingress-tls.yaml`) + +External access via HTTPS with automatic certificate management. + +```bash +helm install jellyfin jellyfin/jellyfin -f examples/ingress-tls.yaml +``` + +**Prerequisites**: + +- Ingress controller (e.g., ingress-nginx) +- cert-manager for automatic TLS certificates (optional) +- DNS record pointing to your cluster + +**Configuration**: + +- Update `jellyfin.example.com` to your actual domain +- Adjust cert-manager issuer if using different CA + +--- + +### 5. NetworkPolicy Security (`network-policy.yaml`) + +Secure deployment with network isolation and controlled traffic flow. + +```bash +helm install jellyfin jellyfin/jellyfin -f examples/network-policy.yaml +``` + +**Prerequisites**: + +- CNI plugin with NetworkPolicy support (Calico, Cilium, Weave, Canal) +- Ingress controller with known namespace/labels + +**Features**: + +- Restrict ingress to Ingress controller only +- Allow egress for DNS and metadata providers (HTTPS/443) +- Block unrestricted pod-to-pod communication + +--- + +## Combining Examples + +You can combine multiple example files to create custom configurations: + +```bash +helm install jellyfin jellyfin/jellyfin \ + -f examples/production.yaml \ + -f examples/hardware-acceleration.yaml \ + -f examples/ingress-tls.yaml +``` + +Files are merged in order, with later files overriding earlier ones. + +## Custom Values + +All examples can be further customized by overriding specific values: + +```bash +helm install jellyfin jellyfin/jellyfin \ + -f examples/production.yaml \ + --set persistence.media.size=1Ti \ + --set resources.limits.memory=16Gi +``` + +## Getting Help + +For more configuration options, see: + +- [Chart README](../README.md) +- [values.yaml](../values.yaml) - Complete configuration reference +- [Jellyfin Documentation](https://jellyfin.org/docs/) diff --git a/charts/jellyfin/examples/hardware-acceleration.yaml b/charts/jellyfin/examples/hardware-acceleration.yaml new file mode 100644 index 0000000..6353585 --- /dev/null +++ b/charts/jellyfin/examples/hardware-acceleration.yaml @@ -0,0 +1,62 @@ +# Hardware Acceleration (Intel QuickSync) +# Enables GPU acceleration for video transcoding +# +# Prerequisites: +# - Node with Intel GPU +# - GPU device plugin installed in cluster +# +# Usage: +# helm install jellyfin jellyfin/jellyfin -f hardware-acceleration.yaml +# +# For other GPU types (AMD/NVIDIA), adjust device paths and security context accordingly. +# See: https://jellyfin.org/docs/general/administration/hardware-acceleration/ + +# Security context for hardware acceleration +securityContext: + # Required capabilities for GPU access + capabilities: + add: + - SYS_ADMIN + drop: + - ALL + privileged: false + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + +# Mount GPU device +volumes: + - name: render-device + hostPath: + path: /dev/dri + type: Directory + +volumeMounts: + - name: render-device + mountPath: /dev/dri + +# Optional: Resource limits for GPU +# Uncomment and adjust for your GPU device plugin +# resources: +# limits: +# gpu.intel.com/i915: 1 +# requests: +# gpu.intel.com/i915: 1 + +# Persistent storage +persistence: + config: + enabled: true + size: 10Gi + media: + enabled: true + size: 500Gi + cache: + enabled: true + size: 50Gi # Larger cache for transcoding + +# Transcoding environment variables (optional) +jellyfin: + env: + - name: JELLYFIN_PublishedServerUrl + value: "https://jellyfin.example.com" diff --git a/charts/jellyfin/examples/ingress-tls.yaml b/charts/jellyfin/examples/ingress-tls.yaml new file mode 100644 index 0000000..a16bb47 --- /dev/null +++ b/charts/jellyfin/examples/ingress-tls.yaml @@ -0,0 +1,58 @@ +# Jellyfin with Ingress and TLS +# External access via HTTPS with certificate management +# +# Prerequisites: +# - Ingress controller installed (e.g., ingress-nginx) +# - cert-manager for TLS certificate automation (optional) +# +# Usage: +# helm install jellyfin jellyfin/jellyfin -f ingress-tls.yaml + +# Ingress configuration +ingress: + enabled: true + className: nginx + annotations: + # cert-manager annotation for automatic certificate provisioning + cert-manager.io/cluster-issuer: letsencrypt-prod + # Recommended nginx settings for Jellyfin + nginx.ingress.kubernetes.io/proxy-body-size: "0" + nginx.ingress.kubernetes.io/proxy-buffering: "off" + nginx.ingress.kubernetes.io/proxy-request-buffering: "off" + nginx.ingress.kubernetes.io/configuration-snippet: | + proxy_set_header X-Forwarded-Protocol $scheme; + proxy_set_header X-Forwarded-Host $http_host; + + hosts: + - host: jellyfin.example.com + paths: + - path: / + pathType: Prefix + + tls: + - secretName: jellyfin-tls + hosts: + - jellyfin.example.com + +# Service configuration +service: + type: ClusterIP + port: 8096 + +# Persistent storage +persistence: + config: + enabled: true + size: 10Gi + media: + enabled: true + size: 500Gi + cache: + enabled: true + size: 20Gi + +# Set published server URL for external access +jellyfin: + env: + - name: JELLYFIN_PublishedServerUrl + value: "https://jellyfin.example.com" diff --git a/charts/jellyfin/examples/minimal.yaml b/charts/jellyfin/examples/minimal.yaml new file mode 100644 index 0000000..b6e3c6f --- /dev/null +++ b/charts/jellyfin/examples/minimal.yaml @@ -0,0 +1,19 @@ +# Minimal Jellyfin deployment +# Quick start with default settings and ephemeral storage +# +# Usage: +# helm install jellyfin jellyfin/jellyfin -f minimal.yaml + +# Use ephemeral storage for testing +persistence: + config: + enabled: false + media: + enabled: false + cache: + enabled: false + +# Basic service configuration +service: + type: ClusterIP + port: 8096 diff --git a/charts/jellyfin/examples/network-policy.yaml b/charts/jellyfin/examples/network-policy.yaml new file mode 100644 index 0000000..2b86c8f --- /dev/null +++ b/charts/jellyfin/examples/network-policy.yaml @@ -0,0 +1,74 @@ +# Secure Jellyfin deployment with NetworkPolicy +# Network isolation with controlled ingress/egress +# +# Prerequisites: +# - CNI plugin with NetworkPolicy support (Calico, Cilium, etc.) +# - Ingress controller running in ingress-nginx namespace +# +# Usage: +# helm install jellyfin jellyfin/jellyfin -f network-policy.yaml + +# NetworkPolicy configuration +networkPolicy: + enabled: true + + # Ingress: Allow only from Ingress controller + ingress: + allowExternal: false + namespaceSelector: + matchLabels: + name: ingress-nginx + podSelector: + matchLabels: + app.kubernetes.io/name: ingress-nginx + + # Egress: Restricted for security + egress: + allowDNS: true # Required for DNS resolution + allowAllEgress: false # Block unrestricted internet + restrictedEgress: + allowMetadata: true # Allow HTTPS/443 for TMDB, TheTVDB, etc. + allowInCluster: false # Block pod-to-pod communication + +# Ingress for external access +ingress: + enabled: true + className: nginx + hosts: + - host: jellyfin.example.com + paths: + - path: / + pathType: Prefix + +# Persistent storage +persistence: + config: + enabled: true + size: 10Gi + media: + enabled: true + size: 500Gi + cache: + enabled: true + size: 20Gi + +# Security context +securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + capabilities: + drop: + - ALL + +# Resource limits +resources: + limits: + cpu: 2000m + memory: 4Gi + requests: + cpu: 500m + memory: 1Gi diff --git a/charts/jellyfin/examples/production.yaml b/charts/jellyfin/examples/production.yaml new file mode 100644 index 0000000..104f4ac --- /dev/null +++ b/charts/jellyfin/examples/production.yaml @@ -0,0 +1,76 @@ +# Production Jellyfin deployment +# Persistent storage, resource limits, and health probes +# +# Usage: +# helm install jellyfin jellyfin/jellyfin -f production.yaml + +# Persistent storage configuration +persistence: + config: + enabled: true + size: 10Gi + storageClass: "" # Use default storage class + accessModes: + - ReadWriteOnce + + media: + enabled: true + size: 500Gi + storageClass: "" + accessModes: + - ReadWriteOnce + + cache: + enabled: true + size: 20Gi + storageClass: "" + accessModes: + - ReadWriteOnce + +# Resource limits and requests +resources: + limits: + cpu: 4000m + memory: 8Gi + requests: + cpu: 1000m + memory: 2Gi + +# Health probes with adjusted timeouts for large libraries +startupProbe: + enabled: true + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 60 + +livenessProbe: + enabled: true + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + +readinessProbe: + enabled: true + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + +# Security context +securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + capabilities: + drop: + - ALL + +# Service configuration +service: + type: ClusterIP + port: 8096 diff --git a/charts/jellyfin/templates/NOTES.txt b/charts/jellyfin/templates/NOTES.txt index 900215c..5d17625 100644 --- a/charts/jellyfin/templates/NOTES.txt +++ b/charts/jellyfin/templates/NOTES.txt @@ -1,15 +1,28 @@ {{- if .Values.initContainers }} ############################################################################# -## ⚠️ WARNING ⚠️ ## +## *** WARNING *** ## ## ## -## The 'initContainers' parameter is DEPRECATED! ## -## Please migrate to 'extraInitContainers' instead. ## +## The 'initContainers' parameter is DEPRECATED! ## +## Please migrate to 'extraInitContainers' instead. ## ## ## -## Support for 'initContainers' will be removed after 2030. ## +## Support for 'initContainers' will be removed after 2030. ## ## ## -## Migration guide: ## -## Old: initContainers: [...] ## -## New: extraInitContainers: [...] ## +## Migration guide: ## +## Old: initContainers: [...] ## +## New: extraInitContainers: [...] ## +## ## +############################################################################# + +{{- end }} +{{- if and .Values.networkPolicy.enabled (or .Values.jellyfin.enableDLNA .Values.podPrivileges.hostNetwork) }} +############################################################################# +## *** ERROR *** ## +## ## +## NetworkPolicy cannot be enabled with hostNetwork or DLNA! ## +## ## +## The deployment will FAIL. Please fix your configuration: ## +## - Disable networkPolicy.enabled, OR ## +## - Disable jellyfin.enableDLNA and podPrivileges.hostNetwork ## ## ## ############################################################################# @@ -18,288 +31,57 @@ 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 }} - -{{- if .Values.httpRoute.enabled }} - -HTTPRoute (Gateway API) is ENABLED: -{{- if .Values.httpRoute.parentRefs }} - Gateway References: - {{- range .Values.httpRoute.parentRefs }} - - {{ .name }}{{ if .namespace }} (namespace: {{ .namespace }}){{ end }}{{ if .sectionName }} [{{ .sectionName }}]{{ end }} - {{- end }} -{{- else }} - ⚠ WARNING: No parentRefs defined - HTTPRoute will not attach to any Gateway! -{{- end }} -{{- if .Values.httpRoute.hostnames }} - Hostnames: - {{- range .Values.httpRoute.hostnames }} - - {{ . }} - {{- end }} +Jellyfin is available via Ingress at: +{{- range .Values.ingress.hosts }} +http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }}{{ (index .paths 0).path }} {{- end }} - Rules: {{ len .Values.httpRoute.rules }} route(s) configured - Backend: {{ include "jellyfin.fullname" . }}:{{ .Values.service.port }} +{{- else if .Values.httpRoute.enabled }} -Access Jellyfin via Gateway API at: +Jellyfin is available via Gateway API HTTPRoute. {{- if .Values.httpRoute.hostnames }} +Hostnames configured: {{- range .Values.httpRoute.hostnames }} - https://{{ . }}{{ (index $.Values.httpRoute.rules 0).matches 0 .path.value }} -{{- end }} -{{- else }} - (Configure httpRoute.hostnames for external access) -{{- end }} - -Note: Ensure your Gateway API CRDs are installed and Gateway is configured. -{{- else if .Values.ingress.enabled }} -{{- if .Values.ingress.enabled }} - -Jellyfin is available via Ingress at: -{{- range .Values.ingress.hosts }} - http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }}{{ (index .paths 0).path }} +- {{ . }} {{- end }} {{- else }} - -To access Jellyfin, you can use port-forwarding: - - $ kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "jellyfin.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }} - -Then visit http://localhost:{{ .Values.service.port }} in your browser. +⚠ No hostnames configured. Set httpRoute.hostnames for external access. {{- end }} +{{- else if contains "NodePort" .Values.service.type }} -{{- if .Values.networkPolicy.enabled }} +Get the Jellyfin URL by running: -Network Policy is ENABLED: -{{- if or .Values.jellyfin.enableDLNA .Values.podPrivileges.hostNetwork }} - WARNING: NetworkPolicy is incompatible with hostNetwork (DLNA mode). - The deployment will FAIL. Please disable networkPolicy.enabled or hostNetwork. -{{- else }} +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 }} - Ingress Policy: - {{- if .Values.networkPolicy.ingress.allowExternal }} - - ✓ Allowing connections from ANY pod in ANY namespace - {{- else }} - {{- if or .Values.networkPolicy.ingress.podSelector .Values.networkPolicy.ingress.namespaceSelector }} - - ✓ Allowing connections ONLY from pods matching: - {{- if .Values.networkPolicy.ingress.podSelector }} - Pod Labels: {{ .Values.networkPolicy.ingress.podSelector | toYaml | nindent 8 }} - {{- end }} - {{- if .Values.networkPolicy.ingress.namespaceSelector }} - Namespace Labels: {{ .Values.networkPolicy.ingress.namespaceSelector | toYaml | nindent 8 }} - {{- end }} - {{- else }} - - ⚠ WARNING: allowExternal=false but no selectors defined - NO pods can connect! - {{- end }} - {{- end }} - {{- if and .Values.metrics.enabled .Values.metrics.serviceMonitor.enabled }} - - ✓ Allowing Prometheus metrics scraping from {{ .Values.networkPolicy.metrics.podSelector | toYaml }} - {{- end }} - {{- if .Values.networkPolicy.ingress.customRules }} - - ✓ Custom ingress rules applied ({{ len .Values.networkPolicy.ingress.customRules }} rules) - {{- end }} +Get the Jellyfin URL by running: - Egress Policy: - {{- if .Values.networkPolicy.egress.allowDNS }} - - ✓ DNS resolution allowed ({{ .Values.networkPolicy.egress.dnsNamespace }}/{{ .Values.networkPolicy.egress.dnsPodSelector | toYaml }}) - {{- else }} - - ⚠ WARNING: DNS is DISABLED - Jellyfin will NOT function properly! - {{- end }} - {{- if .Values.networkPolicy.egress.allowAllEgress }} - - ✓ All egress allowed (metadata, subtitles, images) - {{- else }} - {{- if .Values.networkPolicy.egress.restrictedEgress.allowMetadata }} - - ✓ HTTPS/443 allowed (metadata providers) - {{- end }} - {{- if .Values.networkPolicy.egress.restrictedEgress.allowInCluster }} - - ✓ In-cluster communication allowed - {{- end }} - {{- if .Values.networkPolicy.egress.restrictedEgress.allowedCIDRs }} - - ✓ Custom CIDRs allowed: {{ .Values.networkPolicy.egress.restrictedEgress.allowedCIDRs | join ", " }} - {{- end }} - {{- if not (or .Values.networkPolicy.egress.restrictedEgress.allowMetadata .Values.networkPolicy.egress.restrictedEgress.allowInCluster .Values.networkPolicy.egress.restrictedEgress.allowedCIDRs) }} - - ⚠ WARNING: No egress rules defined - Jellyfin cannot download metadata! - {{- end }} - {{- end }} - {{- if .Values.networkPolicy.egress.customRules }} - - ✓ Custom egress rules applied ({{ len .Values.networkPolicy.egress.customRules }} rules) - {{- end }} +NOTE: It may take a few minutes for the LoadBalancer IP to be available. - Note: NetworkPolicy requires a CNI plugin with NetworkPolicy support (Calico, Cilium, etc.) -{{- end }} +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 }} -Network Policy is DISABLED (default). -To enable network isolation, set networkPolicy.enabled=true in your values. -{{- end }} -For troubleshooting common issues, see the README: -- inotify instance limit errors -- Hardware acceleration setup -- Network configuration - -Useful resources: - Documentation: https://jellyfin.org/docs/ - Troubleshooting: See README.md in the chart repository - -{{- if .Values.jellyfin.envFrom }} +To access Jellyfin, use port-forwarding: -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 }} +kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "jellyfin.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }} -{{- 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 }} +Then visit http://localhost:{{ .Values.service.port }} in your browser. {{- 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 }} +{{- if not .Values.persistence.config.enabled }} - Benefits: - - Faster transcoding and metadata operations - - Separate cache lifecycle from config/media - - Can use faster storage class (SSD) for cache +⚠ WARNING: Configuration persistence is disabled. Data will be lost on pod restart. {{- end }} +{{- if not .Values.persistence.media.enabled }} -{{- 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 +⚠ WARNING: Media persistence is disabled. Media files will be lost on pod restart. {{- end }} -{{- 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 }} - {{- if eq $limit 0 }} - ⚠ WARNING: Revision history is disabled - rollback will not be possible! - {{- else if eq $limit 1 }} - ⚠ NOTE: Only 1 revision kept - limited rollback capability - {{- else }} - You can rollback to any of the last {{ .Values.revisionHistoryLimit }} revisions using: - kubectl rollout undo deployment/{{ include "jellyfin.fullname" . }} --namespace {{ .Release.Namespace }} - {{- end }} +For more information: +Chart documentation: https://github.com/jellyfin/jellyfin-helm +Jellyfin docs: https://jellyfin.org/docs/ +Get release info: helm status {{ .Release.Name }} -n {{ .Release.Namespace }} diff --git a/charts/jellyfin/templates/deployment.yaml b/charts/jellyfin/templates/deployment.yaml index 6f4b6ae..f919231 100644 --- a/charts/jellyfin/templates/deployment.yaml +++ b/charts/jellyfin/templates/deployment.yaml @@ -14,8 +14,8 @@ spec: {{- toYaml . | trim | nindent 4 }} {{- end }} replicas: {{ .Values.replicaCount }} - {{- with .Values.revisionHistoryLimit }} - revisionHistoryLimit: {{ . }} + {{- if not (eq .Values.revisionHistoryLimit nil) }} + revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} {{- end }} selector: matchLabels: @@ -37,8 +37,11 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "jellyfin.serviceAccountName" . }} + automountServiceAccountToken: {{ .Values.serviceAccount.automount }} + {{- with .Values.podSecurityContext }} securityContext: - {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- toYaml . | nindent 8 }} + {{- end }} {{- if or .Values.jellyfin.enableDLNA .Values.podPrivileges.hostNetwork }} hostNetwork: true {{- end }} @@ -50,8 +53,10 @@ spec: {{- end }} containers: - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} + {{- toYaml . | nindent 12 }} + {{- end }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} {{- with .Values.jellyfin.command }} @@ -123,8 +128,10 @@ spec: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: {{- toYaml .Values.readinessProbe | nindent 12 }} + {{- with .Values.resources }} resources: - {{- toYaml .Values.resources | nindent 12 }} + {{- toYaml . | nindent 12 }} + {{- end }} volumeMounts: - mountPath: /config name: config @@ -212,6 +219,8 @@ spec: dnsConfig: {{- toYaml . | nindent 8}} {{- end }} - {{- with .Values.dnsPolicy }} - dnsPolicy: {{ . | quote }} + {{- if .Values.dnsPolicy }} + dnsPolicy: {{ .Values.dnsPolicy | quote }} + {{- else if or .Values.jellyfin.enableDLNA .Values.podPrivileges.hostNetwork }} + dnsPolicy: ClusterFirstWithHostNet {{- end }} diff --git a/charts/jellyfin/tests/cache_persistence_test.yaml b/charts/jellyfin/tests/cache_persistence_test.yaml deleted file mode 100644 index fafcf9e..0000000 --- a/charts/jellyfin/tests/cache_persistence_test.yaml +++ /dev/null @@ -1,160 +0,0 @@ -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/deployment_image_test.yaml b/charts/jellyfin/tests/deployment_image_test.yaml new file mode 100644 index 0000000..8fce544 --- /dev/null +++ b/charts/jellyfin/tests/deployment_image_test.yaml @@ -0,0 +1,319 @@ +suite: test deployment image configuration +templates: + - deployment.yaml +tests: + # ============================================================================= + # IMAGE REPOSITORY + # ============================================================================= + + - it: should use default image repository + asserts: + - matchRegex: + path: spec.template.spec.containers[0].image + pattern: "^docker\\.io/jellyfin/jellyfin:" + + - it: should use custom image repository + set: + image.repository: ghcr.io/jellyfin/jellyfin + asserts: + - matchRegex: + path: spec.template.spec.containers[0].image + pattern: "^ghcr\\.io/jellyfin/jellyfin:" + + - it: should use private registry + set: + image.repository: registry.example.com/jellyfin/jellyfin + asserts: + - matchRegex: + path: spec.template.spec.containers[0].image + pattern: "^registry\\.example\\.com/jellyfin/jellyfin:" + + # ============================================================================= + # IMAGE TAG + # ============================================================================= + + - it: should use Chart.AppVersion as default tag + asserts: + - matchRegex: + path: spec.template.spec.containers[0].image + pattern: ":[0-9]+\\.[0-9]+\\.[0-9]+$" + + - it: should use custom tag when specified + set: + image.tag: 10.8.13 + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: docker.io/jellyfin/jellyfin:10.8.13 + + - it: should support latest tag + set: + image.tag: latest + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: docker.io/jellyfin/jellyfin:latest + + - it: should support beta tags + set: + image.tag: 10.9.0-beta1 + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: docker.io/jellyfin/jellyfin:10.9.0-beta1 + + - it: should support sha256 digest + set: + image.tag: sha256:abc123def456 + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: docker.io/jellyfin/jellyfin:sha256:abc123def456 + + - it: should combine custom repository and tag + set: + image.repository: ghcr.io/custom/jellyfin + image.tag: custom-1.0 + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: ghcr.io/custom/jellyfin:custom-1.0 + + # ============================================================================= + # IMAGE PULL POLICY + # ============================================================================= + + - it: should use IfNotPresent as default pullPolicy + asserts: + - equal: + path: spec.template.spec.containers[0].imagePullPolicy + value: IfNotPresent + + - it: should set Always pullPolicy + set: + image.pullPolicy: Always + asserts: + - equal: + path: spec.template.spec.containers[0].imagePullPolicy + value: Always + + - it: should set Never pullPolicy + set: + image.pullPolicy: Never + asserts: + - equal: + path: spec.template.spec.containers[0].imagePullPolicy + value: Never + + - it: should explicitly set IfNotPresent pullPolicy + set: + image.pullPolicy: IfNotPresent + asserts: + - equal: + path: spec.template.spec.containers[0].imagePullPolicy + value: IfNotPresent + + # ============================================================================= + # IMAGE PULL SECRETS + # ============================================================================= + + - it: should not have imagePullSecrets by default + asserts: + - isNull: + path: spec.template.spec.imagePullSecrets + + - it: should add single imagePullSecret + set: + imagePullSecrets: + - name: regcred + asserts: + - lengthEqual: + path: spec.template.spec.imagePullSecrets + count: 1 + - equal: + path: spec.template.spec.imagePullSecrets[0].name + value: regcred + + - it: should add multiple imagePullSecrets + set: + imagePullSecrets: + - name: regcred-1 + - name: regcred-2 + - name: regcred-3 + asserts: + - lengthEqual: + path: spec.template.spec.imagePullSecrets + count: 3 + - equal: + path: spec.template.spec.imagePullSecrets[0].name + value: regcred-1 + - equal: + path: spec.template.spec.imagePullSecrets[1].name + value: regcred-2 + - equal: + path: spec.template.spec.imagePullSecrets[2].name + value: regcred-3 + + - it: should support Docker Hub secrets + set: + imagePullSecrets: + - name: dockerhub-secret + asserts: + - equal: + path: spec.template.spec.imagePullSecrets[0].name + value: dockerhub-secret + + - it: should support GitHub Container Registry secrets + set: + imagePullSecrets: + - name: ghcr-secret + asserts: + - equal: + path: spec.template.spec.imagePullSecrets[0].name + value: ghcr-secret + + - it: should support private registry secrets + set: + imagePullSecrets: + - name: private-registry-secret + asserts: + - equal: + path: spec.template.spec.imagePullSecrets[0].name + value: private-registry-secret + + # ============================================================================= + # EDGE CASES + # ============================================================================= + + - it: should handle empty imagePullSecrets array + set: + imagePullSecrets: [] + asserts: + - isNull: + path: spec.template.spec.imagePullSecrets + + - it: should handle empty tag (use default) + set: + image.tag: "" + asserts: + - matchRegex: + path: spec.template.spec.containers[0].image + pattern: ":[0-9]+\\.[0-9]+\\.[0-9]+$" + + # ============================================================================= + # COMPLETE SCENARIOS + # ============================================================================= + + - it: should configure for private registry with authentication + set: + image.repository: registry.company.com/jellyfin/jellyfin + image.tag: 10.8.13-enterprise + image.pullPolicy: Always + imagePullSecrets: + - name: company-registry-secret + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: registry.company.com/jellyfin/jellyfin:10.8.13-enterprise + - equal: + path: spec.template.spec.containers[0].imagePullPolicy + value: Always + - lengthEqual: + path: spec.template.spec.imagePullSecrets + count: 1 + - equal: + path: spec.template.spec.imagePullSecrets[0].name + value: company-registry-secret + + - it: should configure for development with latest tag + set: + image.repository: jellyfin/jellyfin + image.tag: latest + image.pullPolicy: Always + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: jellyfin/jellyfin:latest + - equal: + path: spec.template.spec.containers[0].imagePullPolicy + value: Always + + - it: should configure for multiple registry secrets + set: + image.repository: multi-registry.example.com/jellyfin + imagePullSecrets: + - name: registry-1-secret + - name: registry-2-secret + - name: backup-registry-secret + asserts: + - lengthEqual: + path: spec.template.spec.imagePullSecrets + count: 3 + + - it: should configure for air-gapped environment + set: + image.repository: internal-registry.local/jellyfin/jellyfin + image.tag: 10.8.13-airgap + image.pullPolicy: IfNotPresent + imagePullSecrets: + - name: internal-registry-creds + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: internal-registry.local/jellyfin/jellyfin:10.8.13-airgap + - equal: + path: spec.template.spec.containers[0].imagePullPolicy + value: IfNotPresent + + # ============================================================================= + # REGRESSION TESTS + # ============================================================================= + + - it: should always have image specified + asserts: + - isNotNull: + path: spec.template.spec.containers[0].image + + - it: should always have pullPolicy specified + asserts: + - isNotNull: + path: spec.template.spec.containers[0].imagePullPolicy + + - it: should construct image correctly with defaults + asserts: + - matchRegex: + path: spec.template.spec.containers[0].image + pattern: '^docker\.io/jellyfin/jellyfin:[0-9]+\.[0-9]+\.[0-9]+$' + + - it: should not have imagePullSecrets when not specified + asserts: + - isNull: + path: spec.template.spec.imagePullSecrets + + # Verify image format with various combinations + - it: should format image with repository without registry + set: + image.repository: jellyfin/jellyfin-custom + image.tag: v1.0 + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: jellyfin/jellyfin-custom:v1.0 + + - it: should format image with full registry path + set: + image.repository: docker.io/jellyfin/jellyfin + image.tag: stable + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: docker.io/jellyfin/jellyfin:stable + + - it: should format image with port in registry + set: + image.repository: registry.example.com:5000/jellyfin/jellyfin + image.tag: custom + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: registry.example.com:5000/jellyfin/jellyfin:custom diff --git a/charts/jellyfin/tests/deployment_jellyfin_test.yaml b/charts/jellyfin/tests/deployment_jellyfin_test.yaml new file mode 100644 index 0000000..42dbe27 --- /dev/null +++ b/charts/jellyfin/tests/deployment_jellyfin_test.yaml @@ -0,0 +1,433 @@ +suite: test jellyfin-specific deployment configuration +templates: + - deployment.yaml +tests: + # ============================================================================= + # DLNA AND HOST NETWORK + # ============================================================================= + + - it: should not enable DLNA by default + asserts: + - isNull: + path: spec.template.spec.hostNetwork + + - it: should enable hostNetwork when DLNA enabled + set: + jellyfin.enableDLNA: true + asserts: + - equal: + path: spec.template.spec.hostNetwork + value: true + + - it: should set dnsPolicy to ClusterFirstWithHostNet when DLNA enabled + set: + jellyfin.enableDLNA: true + asserts: + - equal: + path: spec.template.spec.dnsPolicy + value: ClusterFirstWithHostNet + + - it: should enable hostNetwork when explicitly set + set: + podPrivileges.hostNetwork: true + asserts: + - equal: + path: spec.template.spec.hostNetwork + value: true + + - it: should enable DLNA with all required settings + set: + jellyfin.enableDLNA: true + asserts: + - equal: + path: spec.template.spec.hostNetwork + value: true + - equal: + path: spec.template.spec.dnsPolicy + value: ClusterFirstWithHostNet + + # ============================================================================= + # METRICS LIFECYCLE + # ============================================================================= + + - it: should not have lifecycle by default + asserts: + - isNull: + path: spec.template.spec.containers[0].lifecycle + + - it: should add postStart hook when metrics enabled + set: + metrics.enabled: true + asserts: + - isNotNull: + path: spec.template.spec.containers[0].lifecycle + - isNotNull: + path: spec.template.spec.containers[0].lifecycle.postStart + + - it: should execute metrics setup script in postStart + set: + metrics.enabled: true + asserts: + - equal: + path: spec.template.spec.containers[0].lifecycle.postStart.exec.command[0] + value: /bin/sh + - contains: + path: spec.template.spec.containers[0].lifecycle.postStart.exec.command + content: -c + + # ============================================================================= + # COMMAND AND ARGS + # ============================================================================= + + - it: should not override command by default + asserts: + - isNull: + path: spec.template.spec.containers[0].command + + - it: should set custom command when specified + set: + jellyfin.command: ['/custom/jellyfin'] + asserts: + - equal: + path: spec.template.spec.containers[0].command[0] + value: /custom/jellyfin + + - it: should set complex command + set: + jellyfin.command: ['/bin/sh', '-c'] + asserts: + - lengthEqual: + path: spec.template.spec.containers[0].command + count: 2 + - equal: + path: spec.template.spec.containers[0].command[0] + value: /bin/sh + - equal: + path: spec.template.spec.containers[0].command[1] + value: -c + + - it: should not override args by default + asserts: + - isNull: + path: spec.template.spec.containers[0].args + + - it: should set custom args when specified + set: + jellyfin.args: ['--debug'] + asserts: + - contains: + path: spec.template.spec.containers[0].args + content: --debug + + - it: should set multiple args + set: + jellyfin.args: ['--debug', '--verbose', '--log-level=trace'] + asserts: + - lengthEqual: + path: spec.template.spec.containers[0].args + count: 3 + - contains: + path: spec.template.spec.containers[0].args + content: --debug + - contains: + path: spec.template.spec.containers[0].args + content: --log-level=trace + + - it: should set both command and args + set: + jellyfin.command: ['/bin/sh', '-c'] + jellyfin.args: ['exec /jellyfin/jellyfin --debug'] + asserts: + - lengthEqual: + path: spec.template.spec.containers[0].command + count: 2 + - lengthEqual: + path: spec.template.spec.containers[0].args + count: 1 + + # ============================================================================= + # ENVIRONMENT VARIABLES + # ============================================================================= + + - it: should not have custom env by default + asserts: + - isNull: + path: spec.template.spec.containers[0].env + + - it: should set single environment variable + set: + jellyfin.env: + - name: JELLYFIN_LOG_LEVEL + value: Debug + asserts: + - lengthEqual: + path: spec.template.spec.containers[0].env + count: 1 + - equal: + path: spec.template.spec.containers[0].env[0].name + value: JELLYFIN_LOG_LEVEL + - equal: + path: spec.template.spec.containers[0].env[0].value + value: Debug + + - it: should set multiple environment variables + set: + jellyfin.env: + - name: JELLYFIN_LOG_LEVEL + value: Debug + - name: JELLYFIN_CACHE_DIR + value: /cache + - name: JELLYFIN_DATA_DIR + value: /config + asserts: + - lengthEqual: + path: spec.template.spec.containers[0].env + count: 3 + + - it: should support env from Secret + set: + jellyfin.env: + - name: API_KEY + valueFrom: + secretKeyRef: + name: jellyfin-secrets + key: api-key + asserts: + - equal: + path: spec.template.spec.containers[0].env[0].name + value: API_KEY + - equal: + path: spec.template.spec.containers[0].env[0].valueFrom.secretKeyRef.name + value: jellyfin-secrets + - equal: + path: spec.template.spec.containers[0].env[0].valueFrom.secretKeyRef.key + value: api-key + + - it: should support env from ConfigMap + set: + jellyfin.env: + - name: CONFIG_VALUE + valueFrom: + configMapKeyRef: + name: jellyfin-config + key: config-value + asserts: + - equal: + path: spec.template.spec.containers[0].env[0].valueFrom.configMapKeyRef.name + value: jellyfin-config + + - it: should support env from fieldRef + set: + jellyfin.env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + asserts: + - equal: + path: spec.template.spec.containers[0].env[0].valueFrom.fieldRef.fieldPath + value: metadata.name + - equal: + path: spec.template.spec.containers[0].env[1].valueFrom.fieldRef.fieldPath + value: metadata.namespace + + - it: should support env from resourceFieldRef + set: + jellyfin.env: + - name: MEMORY_LIMIT + valueFrom: + resourceFieldRef: + containerName: jellyfin + resource: limits.memory + asserts: + - equal: + path: spec.template.spec.containers[0].env[0].valueFrom.resourceFieldRef.resource + value: limits.memory + + # ============================================================================= + # CONTAINER PORTS + # ============================================================================= + + - it: should expose default http port + asserts: + - lengthEqual: + path: spec.template.spec.containers[0].ports + count: 1 + - equal: + path: spec.template.spec.containers[0].ports[0].name + value: http + - equal: + path: spec.template.spec.containers[0].ports[0].containerPort + value: 8096 + - equal: + path: spec.template.spec.containers[0].ports[0].protocol + value: TCP + + - it: should keep containerPort 8096 regardless of service port + set: + service.port: 9096 + asserts: + - equal: + path: spec.template.spec.containers[0].ports[0].containerPort + value: 8096 + + # ============================================================================= + # EDGE CASES + # ============================================================================= + + - it: should handle empty command array + set: + jellyfin.command: [] + asserts: + - isNull: + path: spec.template.spec.containers[0].command + + - it: should handle empty args array + set: + jellyfin.args: [] + asserts: + - isNull: + path: spec.template.spec.containers[0].args + + - it: should handle empty env array + set: + jellyfin.env: [] + asserts: + - isNull: + path: spec.template.spec.containers[0].env + + # Mix of different env types + - it: should support mix of env value types + set: + jellyfin.env: + - name: DIRECT_VALUE + value: test + - name: SECRET_VALUE + valueFrom: + secretKeyRef: + name: my-secret + key: secret-key + - name: CONFIG_VALUE + valueFrom: + configMapKeyRef: + name: my-config + key: config-key + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + asserts: + - lengthEqual: + path: spec.template.spec.containers[0].env + count: 4 + - equal: + path: spec.template.spec.containers[0].env[0].value + value: test + - isNotNull: + path: spec.template.spec.containers[0].env[1].valueFrom.secretKeyRef + - isNotNull: + path: spec.template.spec.containers[0].env[2].valueFrom.configMapKeyRef + - isNotNull: + path: spec.template.spec.containers[0].env[3].valueFrom.fieldRef + + # ============================================================================= + # COMPLETE SCENARIOS + # ============================================================================= + + - it: should configure for DLNA with custom env + set: + jellyfin.enableDLNA: true + jellyfin.env: + - name: JELLYFIN_PublishedServerUrl + value: http://jellyfin.local:8096 + asserts: + - equal: + path: spec.template.spec.hostNetwork + value: true + - equal: + path: spec.template.spec.dnsPolicy + value: ClusterFirstWithHostNet + - lengthEqual: + path: spec.template.spec.containers[0].env + count: 1 + + - it: should configure with metrics and custom command + set: + metrics.enabled: true + jellyfin.command: ['/custom/jellyfin'] + jellyfin.args: ['--enable-metrics'] + asserts: + - isNotNull: + path: spec.template.spec.containers[0].lifecycle.postStart + - equal: + path: spec.template.spec.containers[0].command[0] + value: /custom/jellyfin + - contains: + path: spec.template.spec.containers[0].args + content: --enable-metrics + + - it: should configure complete custom jellyfin setup + set: + jellyfin.command: ['/jellyfin/jellyfin'] + jellyfin.args: ['--service'] + jellyfin.env: + - name: JELLYFIN_LOG_LEVEL + value: Information + - name: JELLYFIN_CACHE_DIR + value: /cache + - name: JELLYFIN_PublishedServerUrl + value: https://jellyfin.example.com + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: jellyfin-db + key: password + service.port: 8096 + asserts: + - equal: + path: spec.template.spec.containers[0].command[0] + value: /jellyfin/jellyfin + - contains: + path: spec.template.spec.containers[0].args + content: --service + - lengthEqual: + path: spec.template.spec.containers[0].env + count: 4 + - equal: + path: spec.template.spec.containers[0].ports[0].containerPort + value: 8096 + + # ============================================================================= + # REGRESSION TESTS + # ============================================================================= + + - it: should always have http port exposed + asserts: + - contains: + path: spec.template.spec.containers[0].ports + content: + name: http + containerPort: 8096 + protocol: TCP + + - it: should always use TCP protocol for http port + asserts: + - equal: + path: spec.template.spec.containers[0].ports[0].protocol + value: TCP + + - it: should not enable hostNetwork by default + asserts: + - isNull: + path: spec.template.spec.hostNetwork + + - it: should not have metrics lifecycle without metrics.enabled + set: + metrics.enabled: false + asserts: + - isNull: + path: spec.template.spec.containers[0].lifecycle diff --git a/charts/jellyfin/tests/deployment_metadata_test.yaml b/charts/jellyfin/tests/deployment_metadata_test.yaml new file mode 100644 index 0000000..ca208ff --- /dev/null +++ b/charts/jellyfin/tests/deployment_metadata_test.yaml @@ -0,0 +1,440 @@ +suite: test deployment metadata and strategy +templates: + - deployment.yaml +tests: + # ============================================================================= + # NAMES + # ============================================================================= + + - it: should have default name + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-jellyfin + + - it: should use nameOverride + set: + nameOverride: custom-name + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-custom-name + + - it: should use fullnameOverride + set: + fullnameOverride: completely-custom + asserts: + - equal: + path: metadata.name + value: completely-custom + + - it: should prefer fullnameOverride over nameOverride + set: + nameOverride: ignored + fullnameOverride: used-name + asserts: + - equal: + path: metadata.name + value: used-name + + # ============================================================================= + # DEPLOYMENT LABELS + # ============================================================================= + + - it: should have default labels + asserts: + - equal: + path: metadata.labels["app.kubernetes.io/name"] + value: jellyfin + - equal: + path: metadata.labels["app.kubernetes.io/instance"] + value: RELEASE-NAME + - isNotNull: + path: metadata.labels["app.kubernetes.io/version"] + - isNotNull: + path: metadata.labels["helm.sh/chart"] + + - it: should propagate labels to pod template + asserts: + - equal: + path: spec.template.metadata.labels["app.kubernetes.io/name"] + value: jellyfin + - equal: + path: spec.template.metadata.labels["app.kubernetes.io/instance"] + value: RELEASE-NAME + + - it: should use labels in selector + asserts: + - equal: + path: spec.selector.matchLabels["app.kubernetes.io/name"] + value: jellyfin + - equal: + path: spec.selector.matchLabels["app.kubernetes.io/instance"] + value: RELEASE-NAME + + # ============================================================================= + # DEPLOYMENT ANNOTATIONS + # ============================================================================= + + - it: should not have deployment annotations by default + asserts: + - isNull: + path: metadata.annotations + + - it: should add deployment annotations when specified + set: + deploymentAnnotations: + deployment.kubernetes.io/revision: "1" + custom-annotation: custom-value + asserts: + - equal: + path: metadata.annotations["deployment.kubernetes.io/revision"] + value: "1" + - equal: + path: metadata.annotations.custom-annotation + value: custom-value + + - it: should support multiple deployment annotations + template: deployment.yaml + set: + deploymentAnnotations: + annotation1: value1 + annotation2: value2 + annotation3: value3 + asserts: + - equal: + path: metadata.annotations.annotation1 + value: value1 + - equal: + path: metadata.annotations.annotation2 + value: value2 + - equal: + path: metadata.annotations.annotation3 + value: value3 + + # ============================================================================= + # POD ANNOTATIONS + # ============================================================================= + + - it: should not have pod annotations by default + asserts: + - isNull: + path: spec.template.metadata.annotations + + - it: should add pod annotations when specified + set: + podAnnotations: + prometheus.io/scrape: "true" + prometheus.io/port: "8096" + asserts: + - equal: + path: spec.template.metadata.annotations["prometheus.io/scrape"] + value: "true" + - equal: + path: spec.template.metadata.annotations["prometheus.io/port"] + value: "8096" + + - it: should support multiple pod annotations + set: + podAnnotations: + vault.hashicorp.com/agent-inject: "true" + vault.hashicorp.com/role: jellyfin + custom-annotation: custom-value + asserts: + - equal: + path: spec.template.metadata.annotations["vault.hashicorp.com/agent-inject"] + value: "true" + - equal: + path: spec.template.metadata.annotations["vault.hashicorp.com/role"] + value: jellyfin + + # ============================================================================= + # POD LABELS + # ============================================================================= + + - it: should have default pod labels + asserts: + - equal: + path: spec.template.metadata.labels["app.kubernetes.io/name"] + value: jellyfin + - equal: + path: spec.template.metadata.labels["app.kubernetes.io/instance"] + value: RELEASE-NAME + + - it: should add custom pod labels when specified + set: + podLabels: + environment: production + team: platform + asserts: + - equal: + path: spec.template.metadata.labels.environment + value: production + - equal: + path: spec.template.metadata.labels.team + value: platform + + - it: should merge custom pod labels with default labels + set: + podLabels: + custom: label + asserts: + - equal: + path: spec.template.metadata.labels["app.kubernetes.io/name"] + value: jellyfin + - equal: + path: spec.template.metadata.labels.custom + value: label + + - it: should support multiple custom pod labels + set: + podLabels: + label1: value1 + label2: value2 + label3: value3 + label4: value4 + asserts: + - equal: + path: spec.template.metadata.labels.label1 + value: value1 + - equal: + path: spec.template.metadata.labels.label4 + value: value4 + + # ============================================================================= + # DEPLOYMENT STRATEGY + # ============================================================================= + + - it: should use RollingUpdate strategy by default + asserts: + - equal: + path: spec.strategy.type + value: RollingUpdate + + - it: should set Recreate strategy + set: + deploymentStrategy.type: Recreate + asserts: + - equal: + path: spec.strategy.type + value: Recreate + + - it: should explicitly set RollingUpdate strategy + set: + deploymentStrategy.type: RollingUpdate + asserts: + - equal: + path: spec.strategy.type + value: RollingUpdate + + - it: should not have rollingUpdate params with Recreate strategy + set: + deploymentStrategy.type: Recreate + asserts: + - isNull: + path: spec.strategy.rollingUpdate + + # ============================================================================= + # REVISION HISTORY + # ============================================================================= + + - it: should have default revisionHistoryLimit of 3 + asserts: + - equal: + path: spec.revisionHistoryLimit + value: 3 + + - it: should set custom revisionHistoryLimit + set: + revisionHistoryLimit: 10 + asserts: + - equal: + path: spec.revisionHistoryLimit + value: 10 + + - it: should set revisionHistoryLimit to 0 + set: + revisionHistoryLimit: 0 + asserts: + - equal: + path: spec.revisionHistoryLimit + value: 0 + + # ============================================================================= + # REPLICA COUNT + # ============================================================================= + + - it: should have default replicaCount of 1 + asserts: + - equal: + path: spec.replicas + value: 1 + + - it: should set custom replicaCount + set: + replicaCount: 2 + asserts: + - equal: + path: spec.replicas + value: 2 + + - it: should support replicaCount of 3 + set: + replicaCount: 3 + asserts: + - equal: + path: spec.replicas + value: 3 + + # Note: Jellyfin doesn't support horizontal scaling well, but chart allows it + # Users should be warned via NOTES.txt if replicaCount > 1 + + # ============================================================================= + # EDGE CASES + # ============================================================================= + + - it: should handle empty podAnnotations object + set: + podAnnotations: {} + asserts: + - isNull: + path: spec.template.metadata.annotations + + - it: should handle empty podLabels object + set: + podLabels: {} + asserts: + - isNotNull: + path: spec.template.metadata.labels["app.kubernetes.io/name"] + + - it: should handle empty deploymentAnnotations object + set: + deploymentAnnotations: {} + asserts: + - isNull: + path: metadata.annotations + + - it: should handle empty nameOverride + set: + nameOverride: "" + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-jellyfin + + # ============================================================================= + # COMPLETE SCENARIOS + # ============================================================================= + + - it: should configure production deployment with full metadata + set: + fullnameOverride: jellyfin-prod + deploymentAnnotations: + deployment.kubernetes.io/revision: "5" + ci.build.number: "1234" + podAnnotations: + prometheus.io/scrape: "true" + prometheus.io/port: "8096" + podLabels: + environment: production + team: media + version: v10.8.13 + deploymentStrategy.type: RollingUpdate + revisionHistoryLimit: 10 + replicaCount: 1 + asserts: + - equal: + path: metadata.name + value: jellyfin-prod + - equal: + path: metadata.annotations["deployment.kubernetes.io/revision"] + value: "5" + - equal: + path: spec.template.metadata.annotations["prometheus.io/scrape"] + value: "true" + - equal: + path: spec.template.metadata.labels.environment + value: production + - equal: + path: spec.strategy.type + value: RollingUpdate + - equal: + path: spec.revisionHistoryLimit + value: 10 + - equal: + path: spec.replicas + value: 1 + + - it: should configure development deployment with Recreate strategy + set: + nameOverride: jellyfin-dev + podLabels: + environment: development + deploymentStrategy.type: Recreate + revisionHistoryLimit: 5 + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-jellyfin-dev + - equal: + path: spec.template.metadata.labels.environment + value: development + - equal: + path: spec.strategy.type + value: Recreate + - equal: + path: spec.revisionHistoryLimit + value: 5 + + - it: should configure with vault injection annotations + set: + podAnnotations: + vault.hashicorp.com/agent-inject: "true" + vault.hashicorp.com/agent-inject-secret-jellyfin: secret/data/jellyfin + vault.hashicorp.com/role: jellyfin-role + asserts: + - equal: + path: spec.template.metadata.annotations["vault.hashicorp.com/agent-inject"] + value: "true" + - equal: + path: spec.template.metadata.annotations["vault.hashicorp.com/role"] + value: jellyfin-role + + # ============================================================================= + # REGRESSION TESTS + # ============================================================================= + + - it: should always have standard Kubernetes labels + asserts: + - isNotNull: + path: metadata.labels["app.kubernetes.io/name"] + - isNotNull: + path: metadata.labels["app.kubernetes.io/instance"] + - isNotNull: + path: metadata.labels["app.kubernetes.io/version"] + - isNotNull: + path: metadata.labels["helm.sh/chart"] + + - it: should always have matching selectors + asserts: + - equal: + path: spec.selector.matchLabels["app.kubernetes.io/name"] + value: jellyfin + - equal: + path: spec.selector.matchLabels["app.kubernetes.io/instance"] + value: RELEASE-NAME + + - it: should always have spec.strategy defined + asserts: + - isNotNull: + path: spec.strategy + + - it: should always have spec.replicas defined + asserts: + - isNotNull: + path: spec.replicas + + - it: should always have spec.revisionHistoryLimit defined + asserts: + - isNotNull: + path: spec.revisionHistoryLimit diff --git a/charts/jellyfin/tests/deployment_resources_test.yaml b/charts/jellyfin/tests/deployment_resources_test.yaml new file mode 100644 index 0000000..a9f604d --- /dev/null +++ b/charts/jellyfin/tests/deployment_resources_test.yaml @@ -0,0 +1,523 @@ +suite: test deployment resources and volumes +templates: + - deployment.yaml +tests: + # ============================================================================= + # RESOURCES + # ============================================================================= + + - it: should not have resources by default + asserts: + - isNull: + path: spec.template.spec.containers[0].resources + + - it: should set CPU requests + set: + resources: + requests: + cpu: 100m + asserts: + - equal: + path: spec.template.spec.containers[0].resources.requests.cpu + value: 100m + + - it: should set memory requests + set: + resources: + requests: + memory: 128Mi + asserts: + - equal: + path: spec.template.spec.containers[0].resources.requests.memory + value: 128Mi + + - it: should set CPU limits + set: + resources: + limits: + cpu: 500m + asserts: + - equal: + path: spec.template.spec.containers[0].resources.limits.cpu + value: 500m + + - it: should set memory limits + set: + resources: + limits: + memory: 512Mi + asserts: + - equal: + path: spec.template.spec.containers[0].resources.limits.memory + value: 512Mi + + - it: should set both requests and limits + set: + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 1000m + memory: 1Gi + asserts: + - equal: + path: spec.template.spec.containers[0].resources.requests.cpu + value: 100m + - equal: + path: spec.template.spec.containers[0].resources.requests.memory + value: 256Mi + - equal: + path: spec.template.spec.containers[0].resources.limits.cpu + value: 1000m + - equal: + path: spec.template.spec.containers[0].resources.limits.memory + value: 1Gi + + - it: should support GPU resources + set: + resources: + limits: + nvidia.com/gpu: 1 + asserts: + - equal: + path: spec.template.spec.containers[0].resources.limits["nvidia.com/gpu"] + value: 1 + + - it: should support ephemeral-storage + set: + resources: + requests: + ephemeral-storage: 1Gi + limits: + ephemeral-storage: 2Gi + asserts: + - equal: + path: spec.template.spec.containers[0].resources.requests.ephemeral-storage + value: 1Gi + - equal: + path: spec.template.spec.containers[0].resources.limits.ephemeral-storage + value: 2Gi + + # ============================================================================= + # EXTRA VOLUMES + # ============================================================================= + + - it: should not have extra volumes by default + asserts: + - lengthEqual: + path: spec.template.spec.volumes + count: 3 # config, media, cache only + + - it: should add extraVolumes when specified + set: + volumes: + - name: extra-storage + emptyDir: {} + asserts: + - lengthEqual: + path: spec.template.spec.volumes + count: 4 + - contains: + path: spec.template.spec.volumes + content: + name: extra-storage + emptyDir: {} + + - it: should add multiple extraVolumes + set: + volumes: + - name: extra-1 + emptyDir: {} + - name: extra-2 + secret: + secretName: my-secret + - name: extra-3 + configMap: + name: my-configmap + asserts: + - lengthEqual: + path: spec.template.spec.volumes + count: 6 + + - it: should support secret volume + set: + volumes: + - name: secret-vol + secret: + secretName: my-secret + optional: false + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: secret-vol + secret: + secretName: my-secret + optional: false + + - it: should support configMap volume + set: + volumes: + - name: config-vol + configMap: + name: my-config + items: + - key: config.yaml + path: config.yaml + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: config-vol + configMap: + name: my-config + items: + - key: config.yaml + path: config.yaml + + - it: should support NFS volume + set: + volumes: + - name: nfs-storage + nfs: + server: nfs-server.example.com + path: /exported/path + readOnly: false + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: nfs-storage + nfs: + server: nfs-server.example.com + path: /exported/path + readOnly: false + + - it: should support PVC volume + set: + volumes: + - name: pvc-storage + persistentVolumeClaim: + claimName: existing-pvc + readOnly: false + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: pvc-storage + persistentVolumeClaim: + claimName: existing-pvc + readOnly: false + + # ============================================================================= + # VOLUME MOUNTS + # ============================================================================= + + - it: should not have extra volume mounts by default + asserts: + - lengthEqual: + path: spec.template.spec.containers[0].volumeMounts + count: 3 # config, media, cache only + + - it: should add volumeMounts when specified + set: + volumeMounts: + - name: extra-storage + mountPath: /extra + asserts: + - lengthEqual: + path: spec.template.spec.containers[0].volumeMounts + count: 4 + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: extra-storage + mountPath: /extra + + - it: should add multiple volumeMounts + set: + volumeMounts: + - name: extra-1 + mountPath: /extra1 + - name: extra-2 + mountPath: /extra2 + readOnly: true + - name: extra-3 + mountPath: /extra3 + subPath: subdir + asserts: + - lengthEqual: + path: spec.template.spec.containers[0].volumeMounts + count: 6 + + - it: should support volumeMount with all options + set: + volumeMounts: + - name: complete-mount + mountPath: /complete + subPath: subdir + readOnly: true + mountPropagation: HostToContainer + asserts: + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: complete-mount + mountPath: /complete + subPath: subdir + readOnly: true + mountPropagation: HostToContainer + + # ============================================================================= + # EXTRA CONTAINERS + # ============================================================================= + + - it: should not have extra containers by default + asserts: + - lengthEqual: + path: spec.template.spec.containers + count: 1 + + - it: should add extraContainers when specified + set: + extraContainers: + - name: sidecar + image: nginx:latest + asserts: + - lengthEqual: + path: spec.template.spec.containers + count: 2 + - equal: + path: spec.template.spec.containers[1].name + value: sidecar + + - it: should add multiple extraContainers + set: + extraContainers: + - name: sidecar-1 + image: nginx:latest + - name: sidecar-2 + image: busybox:latest + asserts: + - lengthEqual: + path: spec.template.spec.containers + count: 3 + + - it: should preserve full sidecar container spec + set: + extraContainers: + - name: metrics-exporter + image: prom/node-exporter:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 9100 + name: metrics + env: + - name: EXPORT_INTERVAL + value: "30s" + volumeMounts: + - name: proc + mountPath: /host/proc + readOnly: true + resources: + requests: + cpu: 50m + memory: 32Mi + limits: + cpu: 100m + memory: 64Mi + volumes: + - name: proc + hostPath: + path: /proc + asserts: + - equal: + path: spec.template.spec.containers[1].name + value: metrics-exporter + - equal: + path: spec.template.spec.containers[1].image + value: prom/node-exporter:latest + - equal: + path: spec.template.spec.containers[1].ports[0].containerPort + value: 9100 + - equal: + path: spec.template.spec.containers[1].env[0].name + value: EXPORT_INTERVAL + - equal: + path: spec.template.spec.containers[1].volumeMounts[0].mountPath + value: /host/proc + - equal: + path: spec.template.spec.containers[1].resources.requests.cpu + value: 50m + + # ============================================================================= + # EDGE CASES AND VALIDATIONS + # ============================================================================= + + - it: should handle empty resources object + set: + resources: {} + asserts: + - isNull: + path: spec.template.spec.containers[0].resources + + - it: should handle empty volumes array + set: + volumes: [] + asserts: + - lengthEqual: + path: spec.template.spec.volumes + count: 3 # Still have default volumes + + - it: should handle empty volumeMounts array + set: + volumeMounts: [] + asserts: + - lengthEqual: + path: spec.template.spec.containers[0].volumeMounts + count: 3 # Still have default mounts + + - it: should handle empty extraContainers array + set: + extraContainers: [] + asserts: + - lengthEqual: + path: spec.template.spec.containers + count: 1 + + # Combine volumes and volumeMounts + - it: should combine extraVolumes and volumeMounts correctly + set: + volumes: + - name: shared-data + emptyDir: {} + volumeMounts: + - name: shared-data + mountPath: /shared + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: shared-data + emptyDir: {} + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: shared-data + mountPath: /shared + + # ============================================================================= + # COMPLETE SCENARIOS + # ============================================================================= + + - it: should configure complete resource-constrained setup + set: + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: 2000m + memory: 4Gi + volumes: + - name: transcode-cache + emptyDir: + medium: Memory + sizeLimit: 8Gi + - name: backup + nfs: + server: backup.example.com + path: /backups/jellyfin + volumeMounts: + - name: transcode-cache + mountPath: /transcode + - name: backup + mountPath: /backup + readOnly: false + asserts: + - isNotNull: + path: spec.template.spec.containers[0].resources + - lengthEqual: + path: spec.template.spec.volumes + count: 5 # config, media, cache + 2 extra + - lengthEqual: + path: spec.template.spec.containers[0].volumeMounts + count: 5 + + - it: should configure sidecar with shared volume + set: + extraContainers: + - name: log-forwarder + image: fluent/fluent-bit:latest + volumeMounts: + - name: logs + mountPath: /fluent-bit/logs + volumes: + - name: logs + emptyDir: {} + volumeMounts: + - name: logs + mountPath: /logs + asserts: + - lengthEqual: + path: spec.template.spec.containers + count: 2 + - equal: + path: spec.template.spec.containers[1].volumeMounts[0].name + value: logs + + # ============================================================================= + # REGRESSION TESTS + # ============================================================================= + + - it: should always have default volumes + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: config + persistentVolumeClaim: + claimName: RELEASE-NAME-jellyfin-config + - contains: + path: spec.template.spec.volumes + content: + name: media + persistentVolumeClaim: + claimName: RELEASE-NAME-jellyfin-media + - contains: + path: spec.template.spec.volumes + content: + name: cache + emptyDir: {} + + - it: should always have default volume mounts + asserts: + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: config + mountPath: /config + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: media + mountPath: /media + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: cache + mountPath: /cache + + - it: should always have jellyfin as first container + set: + extraContainers: + - name: sidecar + image: nginx:latest + asserts: + - equal: + path: spec.template.spec.containers[0].name + value: jellyfin + - equal: + path: spec.template.spec.containers[1].name + value: sidecar diff --git a/charts/jellyfin/tests/deployment_scheduling_test.yaml b/charts/jellyfin/tests/deployment_scheduling_test.yaml new file mode 100644 index 0000000..e63d30b --- /dev/null +++ b/charts/jellyfin/tests/deployment_scheduling_test.yaml @@ -0,0 +1,201 @@ +suite: test deployment scheduling +templates: + - deployment.yaml +tests: + # NODE SELECTOR + - it: should not have nodeSelector by default + asserts: + - isNull: + path: spec.template.spec.nodeSelector + + - it: should set single nodeSelector + set: + nodeSelector: + disktype: ssd + asserts: + - equal: + path: spec.template.spec.nodeSelector.disktype + value: ssd + + - it: should set multiple nodeSelectors + set: + nodeSelector: + disktype: ssd + zone: us-west1-a + instance-type: n1-standard-4 + asserts: + - equal: + path: spec.template.spec.nodeSelector.disktype + value: ssd + - equal: + path: spec.template.spec.nodeSelector.zone + value: us-west1-a + + # TOLERATIONS + - it: should not have tolerations by default + asserts: + - isNull: + path: spec.template.spec.tolerations + + - it: should set toleration with Equal operator + set: + tolerations: + - key: key1 + operator: Equal + value: value1 + effect: NoSchedule + asserts: + - lengthEqual: + path: spec.template.spec.tolerations + count: 1 + - equal: + path: spec.template.spec.tolerations[0].key + value: key1 + - equal: + path: spec.template.spec.tolerations[0].operator + value: Equal + + - it: should set toleration with Exists operator + set: + tolerations: + - key: special + operator: Exists + effect: NoExecute + asserts: + - equal: + path: spec.template.spec.tolerations[0].operator + value: Exists + + - it: should support multiple tolerations + set: + tolerations: + - key: key1 + operator: Equal + value: value1 + effect: NoSchedule + - key: key2 + operator: Exists + effect: NoExecute + - key: key3 + operator: Equal + value: value3 + effect: PreferNoSchedule + asserts: + - lengthEqual: + path: spec.template.spec.tolerations + count: 3 + + # AFFINITY + - it: should not have affinity by default + asserts: + - isNull: + path: spec.template.spec.affinity + + - it: should set nodeAffinity + set: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: disktype + operator: In + values: [ssd] + asserts: + - isNotNull: + path: spec.template.spec.affinity.nodeAffinity + + - it: should set podAffinity + set: + affinity: + podAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app: cache + topologyKey: kubernetes.io/hostname + asserts: + - isNotNull: + path: spec.template.spec.affinity.podAffinity + + - it: should set podAntiAffinity + set: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app: jellyfin + topologyKey: kubernetes.io/hostname + asserts: + - isNotNull: + path: spec.template.spec.affinity.podAntiAffinity + + # DNS CONFIGURATION + - it: should not have dnsPolicy set by default (Kubernetes uses ClusterFirst) + asserts: + - isNull: + path: spec.template.spec.dnsPolicy + + - it: should set custom dnsPolicy + set: + dnsPolicy: None + asserts: + - equal: + path: spec.template.spec.dnsPolicy + value: None + + - it: should not have dnsConfig by default + asserts: + - isNull: + path: spec.template.spec.dnsConfig + + - it: should set dnsConfig when specified + set: + dnsConfig: + nameservers: [1.1.1.1, 8.8.8.8] + searches: [example.com] + options: + - name: ndots + value: "2" + asserts: + - contains: + path: spec.template.spec.dnsConfig.nameservers + content: 1.1.1.1 + - contains: + path: spec.template.spec.dnsConfig.searches + content: example.com + + # COMPLETE SCENARIOS + - it: should combine nodeSelector, tolerations, and affinity + set: + nodeSelector: + disktype: ssd + tolerations: + - key: dedicated + operator: Equal + value: jellyfin + effect: NoSchedule + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app.kubernetes.io/name: jellyfin + topologyKey: kubernetes.io/hostname + asserts: + - isNotNull: + path: spec.template.spec.nodeSelector + - lengthEqual: + path: spec.template.spec.tolerations + count: 1 + - isNotNull: + path: spec.template.spec.affinity.podAntiAffinity + + # REGRESSION + - it: should not set dnsPolicy when hostNetwork not enabled + asserts: + - isNull: + path: spec.template.spec.dnsPolicy diff --git a/charts/jellyfin/tests/deployment_security_test.yaml b/charts/jellyfin/tests/deployment_security_test.yaml new file mode 100644 index 0000000..6619682 --- /dev/null +++ b/charts/jellyfin/tests/deployment_security_test.yaml @@ -0,0 +1,175 @@ +suite: test deployment security contexts and privileges +templates: + - deployment.yaml +tests: + # POD SECURITY CONTEXT + - it: should not have podSecurityContext by default + asserts: + - isNull: + path: spec.template.spec.securityContext + + - it: should set podSecurityContext when specified + set: + podSecurityContext: + fsGroup: 1000 + runAsUser: 1000 + runAsGroup: 1000 + asserts: + - equal: + path: spec.template.spec.securityContext.fsGroup + value: 1000 + - equal: + path: spec.template.spec.securityContext.runAsUser + value: 1000 + + - it: should support runAsNonRoot + set: + podSecurityContext: + runAsNonRoot: true + asserts: + - equal: + path: spec.template.spec.securityContext.runAsNonRoot + value: true + + # CONTAINER SECURITY CONTEXT + - it: should not have container securityContext by default + asserts: + - isNull: + path: spec.template.spec.containers[0].securityContext + + - it: should set container securityContext when specified + set: + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + asserts: + - equal: + path: spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation + value: false + - equal: + path: spec.template.spec.containers[0].securityContext.readOnlyRootFilesystem + value: true + + - it: should support capabilities + set: + securityContext: + capabilities: + drop: [ALL] + add: [NET_BIND_SERVICE] + asserts: + - contains: + path: spec.template.spec.containers[0].securityContext.capabilities.drop + content: ALL + - contains: + path: spec.template.spec.containers[0].securityContext.capabilities.add + content: NET_BIND_SERVICE + + # POD PRIVILEGES + - it: should not enable hostNetwork by default + asserts: + - isNull: + path: spec.template.spec.hostNetwork + + - it: should enable hostNetwork when specified + set: + podPrivileges.hostNetwork: true + asserts: + - equal: + path: spec.template.spec.hostNetwork + value: true + + - it: should set dnsPolicy to ClusterFirstWithHostNet when hostNetwork enabled + set: + podPrivileges.hostNetwork: true + asserts: + - equal: + path: spec.template.spec.dnsPolicy + value: ClusterFirstWithHostNet + + - it: should not enable hostIPC by default + asserts: + - isNull: + path: spec.template.spec.hostIPC + + - it: should enable hostIPC when specified + set: + podPrivileges.hostIPC: true + asserts: + - equal: + path: spec.template.spec.hostIPC + value: true + + - it: should not enable hostPID by default + asserts: + - isNull: + path: spec.template.spec.hostPID + + - it: should enable hostPID when specified + set: + podPrivileges.hostPID: true + asserts: + - equal: + path: spec.template.spec.hostPID + value: true + + # RUNTIME AND PRIORITY CLASSES + - it: should not set runtimeClassName by default + asserts: + - isNull: + path: spec.template.spec.runtimeClassName + + - it: should set runtimeClassName when specified + set: + runtimeClassName: nvidia + asserts: + - equal: + path: spec.template.spec.runtimeClassName + value: nvidia + + - it: should not set priorityClassName by default + asserts: + - isNull: + path: spec.template.spec.priorityClassName + + - it: should set priorityClassName when specified + set: + priorityClassName: high-priority + asserts: + - equal: + path: spec.template.spec.priorityClassName + value: high-priority + + # COMPLETE SCENARIOS + - it: should configure for maximum security + set: + podSecurityContext: + fsGroup: 1000 + runAsUser: 1000 + runAsNonRoot: true + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true + capabilities: + drop: [ALL] + asserts: + - equal: + path: spec.template.spec.securityContext.runAsNonRoot + value: true + - equal: + path: spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation + value: false + + - it: should configure for DLNA with hostNetwork + set: + jellyfin.enableDLNA: true + podPrivileges.hostNetwork: true + asserts: + - equal: + path: spec.template.spec.hostNetwork + value: true + - equal: + path: spec.template.spec.dnsPolicy + value: ClusterFirstWithHostNet diff --git a/charts/jellyfin/tests/envfrom_test.yaml b/charts/jellyfin/tests/envfrom_test.yaml index a5f7165..27288a3 100644 --- a/charts/jellyfin/tests/envfrom_test.yaml +++ b/charts/jellyfin/tests/envfrom_test.yaml @@ -120,3 +120,151 @@ tests: - equal: path: spec.template.spec.containers[0].envFrom[0].configMapRef.name value: jellyfin-config + + # ============================================================================= + # EDGE CASES AND ADDITIONAL VALIDATIONS + # ============================================================================= + + - it: should handle empty envFrom array + set: + jellyfin.envFrom: [] + asserts: + - isNull: + path: spec.template.spec.containers[0].envFrom + + - it: should support both configMapRef and secretRef with optional and prefix + set: + jellyfin.envFrom: + - configMapRef: + name: my-config + optional: true + prefix: CFG_ + - secretRef: + name: my-secret + optional: false + prefix: SEC_ + asserts: + - lengthEqual: + path: spec.template.spec.containers[0].envFrom + count: 2 + - equal: + path: spec.template.spec.containers[0].envFrom[0].configMapRef.name + value: my-config + - equal: + path: spec.template.spec.containers[0].envFrom[0].configMapRef.optional + value: true + - equal: + path: spec.template.spec.containers[0].envFrom[0].prefix + value: CFG_ + - equal: + path: spec.template.spec.containers[0].envFrom[1].secretRef.name + value: my-secret + - equal: + path: spec.template.spec.containers[0].envFrom[1].secretRef.optional + value: false + - equal: + path: spec.template.spec.containers[0].envFrom[1].prefix + value: SEC_ + + - it: should support multiple ConfigMaps with different prefixes + set: + jellyfin.envFrom: + - configMapRef: + name: config-1 + prefix: APP1_ + - configMapRef: + name: config-2 + prefix: APP2_ + - configMapRef: + name: config-3 + prefix: APP3_ + asserts: + - lengthEqual: + path: spec.template.spec.containers[0].envFrom + count: 3 + - equal: + path: spec.template.spec.containers[0].envFrom[0].prefix + value: APP1_ + - equal: + path: spec.template.spec.containers[0].envFrom[1].prefix + value: APP2_ + - equal: + path: spec.template.spec.containers[0].envFrom[2].prefix + value: APP3_ + + - it: should support multiple Secrets with different prefixes + set: + jellyfin.envFrom: + - secretRef: + name: secret-1 + prefix: DB_ + - secretRef: + name: secret-2 + prefix: API_ + asserts: + - lengthEqual: + path: spec.template.spec.containers[0].envFrom + count: 2 + - equal: + path: spec.template.spec.containers[0].envFrom[0].prefix + value: DB_ + - equal: + path: spec.template.spec.containers[0].envFrom[1].prefix + value: API_ + + - it: should not have prefix when not specified + set: + jellyfin.envFrom: + - configMapRef: + name: no-prefix-config + asserts: + - isNull: + path: spec.template.spec.containers[0].envFrom[0].prefix + + - it: should not have optional field when not specified + set: + jellyfin.envFrom: + - configMapRef: + name: required-config + asserts: + - isNull: + path: spec.template.spec.containers[0].envFrom[0].configMapRef.optional + + # Regression: verify envFrom doesn't interfere with other env settings + - it: should preserve JELLYFIN_PublishedServerUrl when envFrom is used + set: + jellyfin.env: + - name: JELLYFIN_PublishedServerUrl + value: https://jellyfin.example.com + jellyfin.envFrom: + - configMapRef: + name: extra-config + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: JELLYFIN_PublishedServerUrl + value: https://jellyfin.example.com + + # Complex scenario with many sources + - it: should support large number of envFrom sources + set: + jellyfin.envFrom: + - configMapRef: + name: config-1 + - secretRef: + name: secret-1 + - configMapRef: + name: config-2 + prefix: PREFIX_ + - secretRef: + name: secret-2 + optional: true + - configMapRef: + name: config-3 + optional: true + prefix: OPT_ + asserts: + - lengthEqual: + path: spec.template.spec.containers[0].envFrom + count: 5 diff --git a/charts/jellyfin/tests/ingress_test.yaml b/charts/jellyfin/tests/ingress_test.yaml new file mode 100644 index 0000000..3161878 --- /dev/null +++ b/charts/jellyfin/tests/ingress_test.yaml @@ -0,0 +1,475 @@ +suite: test ingress +templates: + - ingress.yaml +tests: + # ============================================================================= + # DEFAULT BEHAVIOR + # ============================================================================= + + - it: should not create Ingress by default + asserts: + - hasDocuments: + count: 0 + + - it: should create Ingress when enabled + set: + ingress.enabled: true + asserts: + - hasDocuments: + count: 1 + - isKind: + of: Ingress + - isAPIVersion: + of: networking.k8s.io/v1 + + - it: should have correct default name + set: + ingress.enabled: true + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-jellyfin + + # ============================================================================= + # INGRESS CLASS + # ============================================================================= + + - it: should not set ingressClassName by default + set: + ingress.enabled: true + asserts: + - isNull: + path: spec.ingressClassName + + - it: should set ingressClassName when specified + set: + ingress.enabled: true + ingress.className: nginx + asserts: + - equal: + path: spec.ingressClassName + value: nginx + + - it: should support various ingress classes + set: + ingress.enabled: true + ingress.className: traefik + asserts: + - equal: + path: spec.ingressClassName + value: traefik + + - it: should handle empty ingressClassName + set: + ingress.enabled: true + ingress.className: "" + asserts: + - isNull: + path: spec.ingressClassName + + # ============================================================================= + # ANNOTATIONS + # ============================================================================= + + - it: should not have annotations by default + set: + ingress.enabled: true + asserts: + - isNull: + path: metadata.annotations + + - it: should add custom annotations + set: + ingress.enabled: true + ingress.annotations: + kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/proxy-body-size: 100m + asserts: + - equal: + path: metadata.annotations["kubernetes.io/ingress.class"] + value: nginx + - equal: + path: metadata.annotations["nginx.ingress.kubernetes.io/proxy-body-size"] + value: 100m + + - it: should support multiple annotations + set: + ingress.enabled: true + ingress.annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + asserts: + - equal: + path: metadata.annotations["cert-manager.io/cluster-issuer"] + value: letsencrypt-prod + - equal: + path: metadata.annotations["nginx.ingress.kubernetes.io/ssl-redirect"] + value: "true" + - equal: + path: metadata.annotations["nginx.ingress.kubernetes.io/force-ssl-redirect"] + value: "true" + + # ============================================================================= + # HOSTS CONFIGURATION + # ============================================================================= + + - it: should have default host configuration + set: + ingress.enabled: true + asserts: + - lengthEqual: + path: spec.rules + count: 1 + - equal: + path: spec.rules[0].host + value: chart-example.local + + - it: should set custom single host + set: + ingress.enabled: true + ingress.hosts: + - host: jellyfin.example.com + paths: + - path: / + pathType: Prefix + asserts: + - equal: + path: spec.rules[0].host + value: jellyfin.example.com + + - it: should support multiple hosts + set: + ingress.enabled: true + ingress.hosts: + - host: jellyfin.example.com + paths: + - path: / + pathType: Prefix + - host: media.example.com + paths: + - path: / + pathType: Prefix + asserts: + - lengthEqual: + path: spec.rules + count: 2 + - equal: + path: spec.rules[0].host + value: jellyfin.example.com + - equal: + path: spec.rules[1].host + value: media.example.com + + - it: should handle multiple paths for single host + set: + ingress.enabled: true + ingress.hosts: + - host: example.com + paths: + - path: /jellyfin + pathType: Prefix + - path: /media + pathType: Exact + asserts: + - lengthEqual: + path: spec.rules[0].http.paths + count: 2 + - equal: + path: spec.rules[0].http.paths[0].path + value: /jellyfin + - equal: + path: spec.rules[0].http.paths[1].path + value: /media + + # ============================================================================= + # PATH TYPES + # ============================================================================= + + - it: should support Prefix pathType + set: + ingress.enabled: true + ingress.hosts: + - host: jellyfin.example.com + paths: + - path: / + pathType: Prefix + asserts: + - equal: + path: spec.rules[0].http.paths[0].pathType + value: Prefix + + - it: should support Exact pathType + set: + ingress.enabled: true + ingress.hosts: + - host: jellyfin.example.com + paths: + - path: /jellyfin + pathType: Exact + asserts: + - equal: + path: spec.rules[0].http.paths[0].pathType + value: Exact + + - it: should support ImplementationSpecific pathType + set: + ingress.enabled: true + ingress.hosts: + - host: jellyfin.example.com + paths: + - path: / + pathType: ImplementationSpecific + asserts: + - equal: + path: spec.rules[0].http.paths[0].pathType + value: ImplementationSpecific + + # ============================================================================= + # BACKEND SERVICE + # ============================================================================= + + - it: should route to correct service and port + set: + ingress.enabled: true + asserts: + - equal: + path: spec.rules[0].http.paths[0].backend.service.name + value: RELEASE-NAME-jellyfin + - equal: + path: spec.rules[0].http.paths[0].backend.service.port.number + value: 8096 + + - it: should use custom service port when configured + set: + ingress.enabled: true + service.port: 9096 + asserts: + - equal: + path: spec.rules[0].http.paths[0].backend.service.port.number + value: 9096 + + # ============================================================================= + # TLS CONFIGURATION + # ============================================================================= + + - it: should not have TLS by default + set: + ingress.enabled: true + asserts: + - isNull: + path: spec.tls + + - it: should add TLS configuration when specified + set: + ingress.enabled: true + ingress.tls: + - secretName: jellyfin-tls + hosts: + - jellyfin.example.com + asserts: + - isNotNull: + path: spec.tls + - lengthEqual: + path: spec.tls + count: 1 + - equal: + path: spec.tls[0].secretName + value: jellyfin-tls + - contains: + path: spec.tls[0].hosts + content: jellyfin.example.com + + - it: should support multiple TLS configurations + set: + ingress.enabled: true + ingress.tls: + - secretName: jellyfin-tls + hosts: + - jellyfin.example.com + - secretName: media-tls + hosts: + - media.example.com + asserts: + - lengthEqual: + path: spec.tls + count: 2 + - equal: + path: spec.tls[0].secretName + value: jellyfin-tls + - equal: + path: spec.tls[1].secretName + value: media-tls + + - it: should support TLS with multiple hosts in single certificate + set: + ingress.enabled: true + ingress.tls: + - secretName: wildcard-tls + hosts: + - jellyfin.example.com + - media.example.com + - streaming.example.com + asserts: + - lengthEqual: + path: spec.tls[0].hosts + count: 3 + - contains: + path: spec.tls[0].hosts + content: jellyfin.example.com + - contains: + path: spec.tls[0].hosts + content: media.example.com + + # ============================================================================= + # EDGE CASES + # ============================================================================= + + # Empty hosts array (edge case - likely misconfiguration) + - it: should handle empty hosts array + set: + ingress.enabled: true + ingress.hosts: [] + asserts: + - lengthEqual: + path: spec.rules + count: 0 + + # Empty paths array (edge case) + - it: should handle empty paths array + set: + ingress.enabled: true + ingress.hosts: + - host: jellyfin.example.com + paths: [] + asserts: + - lengthEqual: + path: spec.rules[0].http.paths + count: 0 + + # Multiple paths with different pathTypes + - it: should support mixed pathTypes in same host + set: + ingress.enabled: true + ingress.hosts: + - host: jellyfin.example.com + paths: + - path: / + pathType: Prefix + - path: /api + pathType: Exact + - path: /media + pathType: ImplementationSpecific + asserts: + - lengthEqual: + path: spec.rules[0].http.paths + count: 3 + - equal: + path: spec.rules[0].http.paths[0].pathType + value: Prefix + - equal: + path: spec.rules[0].http.paths[1].pathType + value: Exact + - equal: + path: spec.rules[0].http.paths[2].pathType + value: ImplementationSpecific + + # ============================================================================= + # COMPLETE SCENARIOS + # ============================================================================= + + - it: should create complete ingress with all features + set: + ingress.enabled: true + ingress.className: nginx + ingress.annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + nginx.ingress.kubernetes.io/proxy-body-size: 1g + ingress.hosts: + - host: jellyfin.example.com + paths: + - path: / + pathType: Prefix + ingress.tls: + - secretName: jellyfin-tls-cert + hosts: + - jellyfin.example.com + asserts: + - equal: + path: spec.ingressClassName + value: nginx + - equal: + path: metadata.annotations["cert-manager.io/cluster-issuer"] + value: letsencrypt-prod + - equal: + path: spec.rules[0].host + value: jellyfin.example.com + - equal: + path: spec.tls[0].secretName + value: jellyfin-tls-cert + + - it: should create multi-host multi-path ingress + set: + ingress.enabled: true + ingress.className: traefik + ingress.hosts: + - host: jellyfin.example.com + paths: + - path: / + pathType: Prefix + - path: /admin + pathType: Exact + - host: media.example.com + paths: + - path: / + pathType: Prefix + ingress.tls: + - secretName: example-com-tls + hosts: + - jellyfin.example.com + - media.example.com + asserts: + - lengthEqual: + path: spec.rules + count: 2 + - lengthEqual: + path: spec.rules[0].http.paths + count: 2 + - lengthEqual: + path: spec.rules[1].http.paths + count: 1 + - lengthEqual: + path: spec.tls[0].hosts + count: 2 + + # ============================================================================= + # REGRESSION TESTS + # ============================================================================= + + - it: should always use networking.k8s.io/v1 API version + set: + ingress.enabled: true + asserts: + - equal: + path: apiVersion + value: networking.k8s.io/v1 + + - it: should use service port number + set: + ingress.enabled: true + service.port: 9999 + asserts: + - equal: + path: spec.rules[0].http.paths[0].backend.service.port.number + value: 9999 + + - it: should have correct label selectors + set: + ingress.enabled: true + asserts: + - equal: + path: metadata.labels["app.kubernetes.io/name"] + value: jellyfin + - equal: + path: metadata.labels["app.kubernetes.io/instance"] + value: RELEASE-NAME diff --git a/charts/jellyfin/tests/init_containers_test.yaml b/charts/jellyfin/tests/init_containers_test.yaml index 7ea5259..e491cb9 100644 --- a/charts/jellyfin/tests/init_containers_test.yaml +++ b/charts/jellyfin/tests/init_containers_test.yaml @@ -117,3 +117,134 @@ tests: content: name: new-way image: alpine:3.18 + + # ============================================================================= + # EDGE CASES AND FULL SPECIFICATION + # ============================================================================= + + - it: should handle empty extraInitContainers array + set: + extraInitContainers: [] + asserts: + - isNull: + path: spec.template.spec.initContainers + + - it: should preserve full container specification with all fields + set: + extraInitContainers: + - name: full-spec-init + image: busybox:1.35 + imagePullPolicy: IfNotPresent + command: ['sh', '-c'] + args: ['echo complete'] + env: + - name: ENV_VAR + value: test + envFrom: + - configMapRef: + name: init-config + volumeMounts: + - name: config + mountPath: /config + - name: media + mountPath: /media + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + securityContext: + runAsUser: 0 + allowPrivilegeEscalation: false + asserts: + - equal: + path: spec.template.spec.initContainers[0].imagePullPolicy + value: IfNotPresent + - equal: + path: spec.template.spec.initContainers[0].env[0].name + value: ENV_VAR + - equal: + path: spec.template.spec.initContainers[0].envFrom[0].configMapRef.name + value: init-config + - lengthEqual: + path: spec.template.spec.initContainers[0].volumeMounts + count: 2 + - equal: + path: spec.template.spec.initContainers[0].resources.requests.cpu + value: 100m + - equal: + path: spec.template.spec.initContainers[0].resources.limits.memory + value: 256Mi + - equal: + path: spec.template.spec.initContainers[0].securityContext.allowPrivilegeEscalation + value: false + + - it: should verify merge order (deprecated first, then extra) + set: + initContainers: + - name: first-deprecated + image: busybox:1.35 + extraInitContainers: + - name: second-extra + image: alpine:3.18 + asserts: + - equal: + path: spec.template.spec.initContainers[0].name + value: first-deprecated + - equal: + path: spec.template.spec.initContainers[1].name + value: second-extra + + - it: should support three or more init containers + set: + extraInitContainers: + - name: init-1 + image: busybox:1.35 + - name: init-2 + image: alpine:3.18 + - name: init-3 + image: ubuntu:22.04 + - name: init-4 + image: debian:12 + asserts: + - lengthEqual: + path: spec.template.spec.initContainers + count: 4 + - equal: + path: spec.template.spec.initContainers[2].name + value: init-3 + - equal: + path: spec.template.spec.initContainers[3].name + value: init-4 + + # Regression: verify initContainers comes first when merged + - it: should always place deprecated initContainers before extraInitContainers + set: + initContainers: + - name: deprecated-init + image: busybox:1.35 + - name: deprecated-init-2 + image: busybox:1.36 + extraInitContainers: + - name: extra-init + image: alpine:3.18 + - name: extra-init-2 + image: alpine:3.19 + asserts: + - lengthEqual: + path: spec.template.spec.initContainers + count: 4 + - equal: + path: spec.template.spec.initContainers[0].name + value: deprecated-init + - equal: + path: spec.template.spec.initContainers[1].name + value: deprecated-init-2 + - equal: + path: spec.template.spec.initContainers[2].name + value: extra-init + - equal: + path: spec.template.spec.initContainers[3].name + value: extra-init-2 diff --git a/charts/jellyfin/tests/integration_common_configs_test.yaml b/charts/jellyfin/tests/integration_common_configs_test.yaml new file mode 100644 index 0000000..e3a894d --- /dev/null +++ b/charts/jellyfin/tests/integration_common_configs_test.yaml @@ -0,0 +1,419 @@ +suite: test integration common configurations +templates: + - deployment.yaml + - service.yaml + - ingress.yaml + - httproute.yaml + - networkpolicy.yaml + - serviceMonitor.yaml + - serviceaccount.yaml + - persistentVolumeClaim.yaml +tests: + # ============================================================================= + # SCENARIO 1: DLNA HOME MEDIA SERVER + # ============================================================================= + + - it: should configure DLNA setup correctly + set: + jellyfin.enableDLNA: true + persistence.media.type: hostPath + persistence.media.hostPath: /mnt/nas/media + service.type: NodePort + service.nodePort: 30096 + asserts: + - template: deployment.yaml + equal: + path: spec.template.spec.hostNetwork + value: true + - template: deployment.yaml + equal: + path: spec.template.spec.dnsPolicy + value: ClusterFirstWithHostNet + - template: deployment.yaml + contains: + path: spec.template.spec.volumes + content: + name: media + hostPath: + path: /mnt/nas/media + type: Directory + - template: service.yaml + equal: + path: spec.type + value: NodePort + - template: networkpolicy.yaml + hasDocuments: + count: 0 # NetworkPolicy incompatible with hostNetwork + + # ============================================================================= + # SCENARIO 2: SECURE PRODUCTION SETUP + # ============================================================================= + + - it: should configure secure production setup + set: + networkPolicy.enabled: true + networkPolicy.egress.allowAllEgress: false + networkPolicy.egress.restrictedEgress: + allowMetadata: true + allowInCluster: true + allowedCIDRs: [10.0.0.0/8] + podSecurityContext: + fsGroup: 1000 + runAsUser: 1000 + runAsNonRoot: true + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + capabilities: + drop: [ALL] + persistence.config.size: 10Gi + persistence.media.size: 500Gi + persistence.cache.enabled: true + persistence.cache.type: pvc + persistence.cache.size: 50Gi + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: 2000m + memory: 4Gi + ingress.enabled: true + ingress.className: nginx + ingress.hosts: + - host: jellyfin.example.com + paths: + - path: / + pathType: Prefix + ingress.tls: + - secretName: jellyfin-tls + hosts: [jellyfin.example.com] + asserts: + - template: networkpolicy.yaml + hasDocuments: + count: 1 + - template: deployment.yaml + equal: + path: spec.template.spec.securityContext.runAsNonRoot + value: true + - template: deployment.yaml + equal: + path: spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation + value: false + - template: persistentVolumeClaim.yaml + hasDocuments: + count: 3 + - template: ingress.yaml + hasDocuments: + count: 1 + + # ============================================================================= + # SCENARIO 3: KUBERNETES WITH PROMETHEUS MONITORING + # ============================================================================= + + - it: should configure full metrics and monitoring + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.interval: 30s + metrics.serviceMonitor.labels: + prometheus: kube-prometheus + networkPolicy.enabled: true + networkPolicy.ingress.customRules: + - from: + - namespaceSelector: + matchLabels: + name: monitoring + ports: + - protocol: TCP + port: 8096 + podAnnotations: + prometheus.io/scrape: "true" + prometheus.io/port: "8096" + asserts: + - template: serviceMonitor.yaml + hasDocuments: + count: 1 + - template: serviceMonitor.yaml + equal: + path: spec.endpoints[0].interval + value: 30s + - template: deployment.yaml + isNotNull: + path: spec.template.spec.containers[0].lifecycle.postStart + - template: networkpolicy.yaml + hasDocuments: + count: 1 + + # ============================================================================= + # SCENARIO 4: IPv6-ONLY CLUSTER + # ============================================================================= + + - it: should configure IPv6-only setup + set: + service.ipFamilyPolicy: SingleStack + service.ipFamilies: [IPv6] + persistence.media.type: pvc + persistence.media.size: 1Ti + asserts: + - template: service.yaml + equal: + path: spec.ipFamilyPolicy + value: SingleStack + - template: service.yaml + equal: + path: spec.ipFamilies[0] + value: IPv6 + - template: persistentVolumeClaim.yaml + documentIndex: 1 + equal: + path: spec.resources.requests.storage + value: 1Ti + + # ============================================================================= + # SCENARIO 5: DUAL-STACK IPv4+IPv6 + # ============================================================================= + + - it: should configure dual-stack networking + set: + service.ipFamilyPolicy: RequireDualStack + service.ipFamilies: [IPv4, IPv6] + service.type: LoadBalancer + service.loadBalancerIP: 203.0.113.42 + asserts: + - template: service.yaml + equal: + path: spec.ipFamilyPolicy + value: RequireDualStack + - template: service.yaml + lengthEqual: + path: spec.ipFamilies + count: 2 + - template: service.yaml + equal: + path: spec.type + value: LoadBalancer + + # ============================================================================= + # SCENARIO 6: DEVELOPMENT/TESTING ENVIRONMENT + # ============================================================================= + + - it: should configure development environment + set: + replicaCount: 1 + persistence.config.enabled: false + persistence.media.enabled: false + persistence.cache.enabled: false + image.tag: latest + image.pullPolicy: Always + revisionHistoryLimit: 1 + deploymentStrategy.type: Recreate + jellyfin.env: + - name: JELLYFIN_LOG_LEVEL + value: Debug + asserts: + - template: deployment.yaml + equal: + path: spec.replicas + value: 1 + - template: deployment.yaml + contains: + path: spec.template.spec.volumes + content: + name: config + emptyDir: {} + - template: deployment.yaml + equal: + path: spec.template.spec.containers[0].imagePullPolicy + value: Always + - template: deployment.yaml + equal: + path: spec.strategy.type + value: Recreate + - template: persistentVolumeClaim.yaml + hasDocuments: + count: 0 + + # ============================================================================= + # SCENARIO 7: HIGH AVAILABILITY (with caveats) + # ============================================================================= + + # Note: Jellyfin doesn't truly support HA, but users might try it + - it: should configure for attempted HA setup + set: + replicaCount: 3 + persistence.config.accessMode: ReadWriteMany + persistence.media.accessMode: ReadWriteMany + persistence.media.type: hostPath + persistence.media.hostPath: /nfs/jellyfin/media + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app.kubernetes.io/name: jellyfin + topologyKey: kubernetes.io/hostname + asserts: + - template: deployment.yaml + equal: + path: spec.replicas + value: 3 + - template: persistentVolumeClaim.yaml + contains: + path: spec.accessModes + documentIndex: 0 + content: ReadWriteMany + - template: deployment.yaml + isNotNull: + path: spec.template.spec.affinity.podAntiAffinity + + # ============================================================================= + # SCENARIO 8: CLOUD PROVIDER SPECIFIC (AWS) + # ============================================================================= + + - it: should configure for AWS with IRSA + set: + serviceAccount.create: true + serviceAccount.annotations: + eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/jellyfin-s3 + persistence.media.type: pvc + persistence.media.storageClass: gp3 + persistence.media.size: 500Gi + service.type: LoadBalancer + service.annotations: + service.beta.kubernetes.io/aws-load-balancer-type: nlb + service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing + ingress.enabled: true + ingress.className: alb + ingress.annotations: + alb.ingress.kubernetes.io/scheme: internet-facing + alb.ingress.kubernetes.io/target-type: ip + asserts: + - template: serviceaccount.yaml + equal: + path: metadata.annotations["eks.amazonaws.com/role-arn"] + value: arn:aws:iam::123456789012:role/jellyfin-s3 + - template: service.yaml + equal: + path: metadata.annotations["service.beta.kubernetes.io/aws-load-balancer-type"] + value: nlb + - template: ingress.yaml + equal: + path: spec.ingressClassName + value: alb + + # ============================================================================= + # SCENARIO 9: GCP WITH WORKLOAD IDENTITY + # ============================================================================= + + - it: should configure for GCP with Workload Identity + set: + serviceAccount.create: true + serviceAccount.annotations: + iam.gke.io/gcp-service-account: jellyfin@project-id.iam.gserviceaccount.com + persistence.media.type: pvc + persistence.media.storageClass: standard-rwo + service.type: LoadBalancer + service.annotations: + cloud.google.com/neg: '{"ingress": true}' + asserts: + - template: serviceaccount.yaml + equal: + path: metadata.annotations["iam.gke.io/gcp-service-account"] + value: jellyfin@project-id.iam.gserviceaccount.com + - template: service.yaml + equal: + path: metadata.annotations["cloud.google.com/neg"] + value: '{"ingress": true}' + + # ============================================================================= + # SCENARIO 10: AIR-GAPPED ENVIRONMENT + # ============================================================================= + + - it: should configure for air-gapped environment + set: + image.repository: internal-registry.local/jellyfin/jellyfin + image.tag: 10.8.13-offline + image.pullPolicy: IfNotPresent + imagePullSecrets: + - name: internal-registry-creds + persistence.media.type: hostPath + persistence.media.hostPath: /mnt/local-storage/media + service.type: NodePort + networkPolicy.enabled: true + networkPolicy.egress.allowAllEgress: false + networkPolicy.egress.restrictedEgress: + allowInCluster: true + asserts: + - template: deployment.yaml + equal: + path: spec.template.spec.containers[0].image + value: internal-registry.local/jellyfin/jellyfin:10.8.13-offline + - template: deployment.yaml + equal: + path: spec.template.spec.imagePullSecrets[0].name + value: internal-registry-creds + - template: networkpolicy.yaml + hasDocuments: + count: 1 + + # ============================================================================= + # SCENARIO 11: MINIMAL RESOURCE-CONSTRAINED SETUP + # ============================================================================= + + - it: should configure minimal resource-constrained setup + set: + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + persistence.config.size: 1Gi + persistence.media.type: emptyDir + persistence.cache.enabled: false + service.type: ClusterIP + asserts: + - template: deployment.yaml + equal: + path: spec.template.spec.containers[0].resources.requests.cpu + value: 100m + - template: persistentVolumeClaim.yaml + hasDocuments: + count: 1 # only config + - template: service.yaml + equal: + path: spec.type + value: ClusterIP + + # ============================================================================= + # SCENARIO 12: GATEWAY API (HTTPRoute) + # ============================================================================= + + - it: should configure Gateway API HTTPRoute + set: + httpRoute.enabled: true + httpRoute.parentRefs: + - name: my-gateway + namespace: gateway-system + sectionName: https + httpRoute.hostnames: + - jellyfin.example.com + - media.example.com + networkPolicy.enabled: true + asserts: + - template: httproute.yaml + hasDocuments: + count: 1 + - template: httproute.yaml + lengthEqual: + path: spec.parentRefs + count: 1 + - template: httproute.yaml + lengthEqual: + path: spec.hostnames + count: 2 + - template: networkpolicy.yaml + hasDocuments: + count: 1 diff --git a/charts/jellyfin/tests/networkpolicy_test.yaml b/charts/jellyfin/tests/networkpolicy_test.yaml index 1d54067..0b5b091 100644 --- a/charts/jellyfin/tests/networkpolicy_test.yaml +++ b/charts/jellyfin/tests/networkpolicy_test.yaml @@ -26,16 +26,14 @@ tests: networkPolicy.enabled: true jellyfin.enableDLNA: true asserts: - - failedTemplate: - errorMessage: "NetworkPolicy cannot be enabled when hostNetwork is enabled" + - failedTemplate: {} - it: should fail when NetworkPolicy enabled with hostNetwork set: networkPolicy.enabled: true podPrivileges.hostNetwork: true asserts: - - failedTemplate: - errorMessage: "NetworkPolicy cannot be enabled when hostNetwork is enabled" + - failedTemplate: {} - it: should have both Ingress and Egress policy types by default set: @@ -111,7 +109,7 @@ tests: - isNotNull: path: spec.ingress[1].from[0].namespaceSelector - equal: - path: spec.ingress[1].from[0].namespaceSelector.matchLabels.kubernetes.io/metadata.name + path: spec.ingress[1].from[0].namespaceSelector.matchLabels["kubernetes.io/metadata.name"] value: monitoring - it: should add custom ingress rules @@ -163,7 +161,7 @@ tests: networkPolicy.egress.dnsNamespace: custom-dns-ns asserts: - equal: - path: spec.egress[0].to[0].namespaceSelector.matchLabels.kubernetes.io/metadata.name + path: spec.egress[0].to[0].namespaceSelector.matchLabels["kubernetes.io/metadata.name"] value: custom-dns-ns - it: should allow custom DNS pod selector @@ -290,8 +288,231 @@ tests: networkPolicy.enabled: true asserts: - equal: - path: spec.podSelector.matchLabels.app\.kubernetes\.io/name + path: spec.podSelector.matchLabels["app.kubernetes.io/name"] value: jellyfin - equal: - path: spec.podSelector.matchLabels.app\.kubernetes\.io/instance + path: spec.podSelector.matchLabels["app.kubernetes.io/instance"] value: RELEASE-NAME + + # ============================================================================= + # EDGE CASES AND ADDITIONAL VALIDATIONS + # ============================================================================= + + # CIDR validations (IPv4 and IPv6) + - it: should accept valid IPv4 CIDR in allowedCIDRs + set: + networkPolicy.enabled: true + networkPolicy.egress.allowAllEgress: false + networkPolicy.egress.restrictedEgress: + allowedCIDRs: + - "10.0.0.0/8" + - "192.168.1.0/24" + asserts: + - contains: + path: spec.egress + content: + to: + - ipBlock: + cidr: 10.0.0.0/8 + - contains: + path: spec.egress + content: + to: + - ipBlock: + cidr: 192.168.1.0/24 + + - it: should accept valid IPv6 CIDR in allowedCIDRs + set: + networkPolicy.enabled: true + networkPolicy.egress.allowAllEgress: false + networkPolicy.egress.restrictedEgress: + allowedCIDRs: + - "2001:db8::/32" + - "fe80::/10" + asserts: + - contains: + path: spec.egress + content: + to: + - ipBlock: + cidr: 2001:db8::/32 + - contains: + path: spec.egress + content: + to: + - ipBlock: + cidr: fe80::/10 + + - it: should accept mix of IPv4 and IPv6 CIDRs + set: + networkPolicy.enabled: true + networkPolicy.egress.allowAllEgress: false + networkPolicy.egress.restrictedEgress: + allowedCIDRs: + - "10.0.0.0/8" + - "2001:db8::/32" + asserts: + - contains: + path: spec.egress + content: + to: + - ipBlock: + cidr: 10.0.0.0/8 + - contains: + path: spec.egress + content: + to: + - ipBlock: + cidr: 2001:db8::/32 + + # Egress restricted scenarios + - it: should allow only metadata when restrictedEgress with allowMetadata only + set: + networkPolicy.enabled: true + networkPolicy.egress.allowAllEgress: false + networkPolicy.egress.restrictedEgress: + allowMetadata: true + allowInCluster: false + asserts: + - contains: + path: spec.egress + content: + to: + - ipBlock: + cidr: 0.0.0.0/0 + ports: + - protocol: TCP + port: 443 + + - it: should not allow in-cluster when allowInCluster is false + set: + networkPolicy.enabled: true + networkPolicy.egress.allowAllEgress: false + networkPolicy.egress.restrictedEgress: + allowMetadata: false + allowInCluster: false + asserts: + - notContains: + path: spec.egress + content: + to: + - podSelector: {} + + - it: should handle empty allowedCIDRs array + set: + networkPolicy.enabled: true + networkPolicy.egress.allowAllEgress: false + networkPolicy.egress.restrictedEgress: + allowedCIDRs: [] + asserts: + - isNotNull: + path: spec.egress + + # Custom ingress rules edge cases + - it: should support multiple custom ingress rules + set: + networkPolicy.enabled: true + networkPolicy.ingress.customRules: + - from: + - namespaceSelector: + matchLabels: + name: frontend + ports: + - protocol: TCP + port: 8096 + - from: + - podSelector: + matchLabels: + app: proxy + ports: + - protocol: TCP + port: 8920 + asserts: + - lengthEqual: + path: spec.ingress + count: 3 # default + 2 custom (when metrics disabled) + + # Regression: verify default egress behavior + - it: should have DNS egress by default + set: + networkPolicy.enabled: true + asserts: + - contains: + path: spec.egress + content: + to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + + # Regression: verify ingress port + - it: should expose correct port in ingress rules + set: + networkPolicy.enabled: true + service.port: 9096 + asserts: + - contains: + path: spec.ingress[0].ports + content: + protocol: TCP + port: http + + # Combining all egress options + - it: should combine all egress options when all enabled + set: + networkPolicy.enabled: true + networkPolicy.egress.allowDNS: true + networkPolicy.egress.allowAllEgress: false + networkPolicy.egress.restrictedEgress: + allowMetadata: true + allowInCluster: true + allowedCIDRs: + - "10.0.0.0/8" + asserts: + # Should have DNS + - contains: + path: spec.egress + content: + to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + # Should have metadata (HTTPS for movie/TV metadata providers) + - contains: + path: spec.egress + content: + to: + - ipBlock: + cidr: 0.0.0.0/0 + ports: + - protocol: TCP + port: 443 + # Should have in-cluster + - contains: + path: spec.egress + content: + to: + - podSelector: {} + # Should have custom CIDR + - contains: + path: spec.egress + content: + to: + - ipBlock: + cidr: 10.0.0.0/8 diff --git a/charts/jellyfin/tests/persistence_test.yaml b/charts/jellyfin/tests/persistence_test.yaml new file mode 100644 index 0000000..daf52ac --- /dev/null +++ b/charts/jellyfin/tests/persistence_test.yaml @@ -0,0 +1,666 @@ +suite: test persistence (config, media, cache) +templates: + - deployment.yaml + - persistentVolumeClaim.yaml +tests: + # ============================================================================= + # CONFIG PERSISTENCE TESTS + # ============================================================================= + + # Default behavior + - it: should create config PVC by default + template: persistentVolumeClaim.yaml + asserts: + - hasDocuments: + count: 2 # config and media (cache disabled by default) + + - it: should set default config PVC size to 5Gi + template: persistentVolumeClaim.yaml + documentIndex: 0 + asserts: + - equal: + path: spec.resources.requests.storage + value: 5Gi + + - it: should set default config PVC access mode to ReadWriteOnce + template: persistentVolumeClaim.yaml + documentIndex: 0 + asserts: + - contains: + path: spec.accessModes + content: ReadWriteOnce + + - it: should mount config PVC by default + template: deployment.yaml + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: config + persistentVolumeClaim: + claimName: RELEASE-NAME-jellyfin-config + + - it: should mount config at /config + template: deployment.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: config + mountPath: /config + + # Custom config PVC settings + - it: should set custom config PVC size + template: persistentVolumeClaim.yaml + documentIndex: 0 + set: + persistence.config.size: 10Gi + asserts: + - equal: + path: spec.resources.requests.storage + value: 10Gi + + - it: should set custom config PVC access mode + template: persistentVolumeClaim.yaml + documentIndex: 0 + set: + persistence.config.accessMode: ReadWriteMany + asserts: + - contains: + path: spec.accessModes + content: ReadWriteMany + + - it: should set custom config PVC storage class + template: persistentVolumeClaim.yaml + documentIndex: 0 + set: + persistence.config.storageClass: fast-ssd + asserts: + - equal: + path: spec.storageClassName + value: fast-ssd + + - it: should add annotations to config PVC + template: persistentVolumeClaim.yaml + documentIndex: 0 + set: + persistence.config.annotations: + backup: "true" + retention: "30d" + asserts: + - equal: + path: metadata.annotations.backup + value: "true" + - equal: + path: metadata.annotations.retention + value: "30d" + + # Config with existing claim + - it: should use existing claim for config when specified + template: deployment.yaml + set: + persistence.config.existingClaim: my-existing-config-claim + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: config + persistentVolumeClaim: + claimName: my-existing-config-claim + + - it: should not create config PVC when existing claim used + template: persistentVolumeClaim.yaml + set: + persistence.config.existingClaim: my-existing-config-claim + asserts: + - hasDocuments: + count: 1 # media only + + # Config disabled (emptyDir fallback) + - it: should use emptyDir for config when disabled + template: deployment.yaml + set: + persistence.config.enabled: false + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: config + emptyDir: {} + + - it: should not create config PVC when disabled + template: persistentVolumeClaim.yaml + set: + persistence.config.enabled: false + asserts: + - hasDocuments: + count: 1 # media only + + # ============================================================================= + # MEDIA PERSISTENCE TESTS + # ============================================================================= + + # Default behavior + - it: should create media PVC by default + template: persistentVolumeClaim.yaml + documentIndex: 1 + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-jellyfin-media + + - it: should set default media PVC size to 25Gi + template: persistentVolumeClaim.yaml + documentIndex: 1 + asserts: + - equal: + path: spec.resources.requests.storage + value: 25Gi + + - it: should set default media PVC access mode to ReadWriteOnce + template: persistentVolumeClaim.yaml + documentIndex: 1 + asserts: + - contains: + path: spec.accessModes + content: ReadWriteOnce + + - it: should mount media PVC by default + template: deployment.yaml + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: media + persistentVolumeClaim: + claimName: RELEASE-NAME-jellyfin-media + + - it: should mount media at /media + template: deployment.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: media + mountPath: /media + + # Media with type: pvc (explicit) + - it: should create media PVC when type is pvc + template: persistentVolumeClaim.yaml + set: + persistence.media.type: pvc + asserts: + - hasDocuments: + count: 2 # config and media + + - it: should set custom media PVC size + template: persistentVolumeClaim.yaml + documentIndex: 1 + set: + persistence.media.type: pvc + persistence.media.size: 100Gi + asserts: + - equal: + path: spec.resources.requests.storage + value: 100Gi + + - it: should set custom media PVC storage class + template: persistentVolumeClaim.yaml + documentIndex: 1 + set: + persistence.media.type: pvc + persistence.media.storageClass: slow-hdd + asserts: + - equal: + path: spec.storageClassName + value: slow-hdd + + - it: should add annotations to media PVC + template: persistentVolumeClaim.yaml + documentIndex: 1 + set: + persistence.media.type: pvc + persistence.media.annotations: + media-type: movies + asserts: + - equal: + path: metadata.annotations.media-type + value: movies + + # Media with type: hostPath + - it: should mount media from hostPath when configured + template: deployment.yaml + set: + persistence.media.type: hostPath + persistence.media.hostPath: /mnt/nas/media + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: media + hostPath: + path: /mnt/nas/media + type: Directory + + - it: should not create media PVC when type is hostPath + template: persistentVolumeClaim.yaml + set: + persistence.media.type: hostPath + persistence.media.hostPath: /mnt/nas/media + asserts: + - hasDocuments: + count: 1 # config only + + # Media with type: emptyDir + - it: should mount media as emptyDir when type is emptyDir + template: deployment.yaml + set: + persistence.media.type: emptyDir + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: media + emptyDir: {} + + - it: should not create media PVC when type is emptyDir + template: persistentVolumeClaim.yaml + set: + persistence.media.type: emptyDir + asserts: + - hasDocuments: + count: 1 # config only + + # Media disabled + - it: should use emptyDir for media when disabled + template: deployment.yaml + set: + persistence.media.enabled: false + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: media + emptyDir: {} + + - it: should not create media PVC when disabled + template: persistentVolumeClaim.yaml + set: + persistence.media.enabled: false + asserts: + - hasDocuments: + count: 1 # config only + + # Media with existing claim + - it: should use existing claim for media when specified + template: deployment.yaml + set: + persistence.media.type: pvc + persistence.media.existingClaim: my-existing-media-claim + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: media + persistentVolumeClaim: + claimName: my-existing-media-claim + + - it: should not create media PVC when existing claim used + template: persistentVolumeClaim.yaml + set: + persistence.media.type: pvc + persistence.media.existingClaim: my-existing-media-claim + asserts: + - hasDocuments: + count: 1 # config only + + # ============================================================================= + # CACHE PERSISTENCE TESTS + # ============================================================================= + + # Default behavior + - it: should mount cache as emptyDir by default + template: deployment.yaml + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: cache + emptyDir: {} + + - it: should not create cache PVC by default + template: persistentVolumeClaim.yaml + asserts: + - hasDocuments: + count: 2 # config and media only + + - it: should mount cache at /cache + template: deployment.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: cache + mountPath: /cache + + # Cache with type: pvc + - it: should create cache PVC when enabled with type pvc + template: persistentVolumeClaim.yaml + set: + persistence.cache.enabled: true + persistence.cache.type: pvc + asserts: + - hasDocuments: + count: 3 # config, media, and cache + + - 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 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 + + # Cache with type: hostPath + - 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 + type: Directory + + - 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 + + # Cache with type: emptyDir + - 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 + + # Cache with existing claim + - it: should use existing claim for cache 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 cache 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 + + # ============================================================================= + # EDGE CASES AND VALIDATIONS + # ============================================================================= + + # All persistence disabled + - it: should use emptyDir for all volumes when all persistence disabled + template: deployment.yaml + set: + persistence.config.enabled: false + persistence.media.enabled: false + persistence.cache.enabled: false + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: config + emptyDir: {} + - contains: + path: spec.template.spec.volumes + content: + name: media + emptyDir: {} + - contains: + path: spec.template.spec.volumes + content: + name: cache + emptyDir: {} + + - it: should not create any PVCs when all persistence disabled + template: persistentVolumeClaim.yaml + set: + persistence.config.enabled: false + persistence.media.enabled: false + persistence.cache.enabled: false + asserts: + - hasDocuments: + count: 0 + + # All persistence enabled as PVC + - it: should create all three PVCs when all enabled + template: persistentVolumeClaim.yaml + set: + persistence.cache.enabled: true + persistence.cache.type: pvc + asserts: + - hasDocuments: + count: 3 # config, media, and cache + + # Mix of persistence types + - it: should handle mix of pvc, hostPath, and emptyDir + template: deployment.yaml + set: + persistence.config.enabled: true + persistence.media.type: hostPath + persistence.media.hostPath: /mnt/media + persistence.cache.enabled: true + persistence.cache.type: emptyDir + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: config + persistentVolumeClaim: + claimName: RELEASE-NAME-jellyfin-config + - contains: + path: spec.template.spec.volumes + content: + name: media + hostPath: + path: /mnt/media + type: Directory + - contains: + path: spec.template.spec.volumes + content: + name: cache + emptyDir: {} + + - it: should create only config PVC with mixed types + template: persistentVolumeClaim.yaml + set: + persistence.media.type: hostPath + persistence.media.hostPath: /mnt/media + persistence.cache.enabled: true + persistence.cache.type: emptyDir + asserts: + - hasDocuments: + count: 1 # config only + + # All access modes + - it: should support ReadWriteOnce access mode + template: persistentVolumeClaim.yaml + documentIndex: 0 + set: + persistence.config.accessMode: ReadWriteOnce + asserts: + - contains: + path: spec.accessModes + content: ReadWriteOnce + + - it: should support ReadOnlyMany access mode + template: persistentVolumeClaim.yaml + documentIndex: 1 + set: + persistence.media.accessMode: ReadOnlyMany + asserts: + - contains: + path: spec.accessModes + content: ReadOnlyMany + + - it: should support ReadWriteMany access mode + template: persistentVolumeClaim.yaml + documentIndex: 0 + set: + persistence.config.accessMode: ReadWriteMany + asserts: + - contains: + path: spec.accessModes + content: ReadWriteMany + + - it: should support ReadWriteOncePod access mode + template: persistentVolumeClaim.yaml + documentIndex: 0 + set: + persistence.config.accessMode: ReadWriteOncePod + asserts: + - contains: + path: spec.accessModes + content: ReadWriteOncePod + + # Empty storageClass (default provisioner) + - it: should not set storageClassName when storageClass is empty string + template: persistentVolumeClaim.yaml + documentIndex: 0 + set: + persistence.config.storageClass: '' + asserts: + - isNull: + path: spec.storageClassName + + # Multiple annotations + - it: should support multiple annotations on PVC + template: persistentVolumeClaim.yaml + documentIndex: 0 + set: + persistence.config.annotations: + annotation1: value1 + annotation2: value2 + annotation3: value3 + asserts: + - equal: + path: metadata.annotations.annotation1 + value: value1 + - equal: + path: metadata.annotations.annotation2 + value: value2 + - equal: + path: metadata.annotations.annotation3 + value: value3 + + # Regression: verify all volume mounts are always present + - it: should always have all three volume mounts in container + template: deployment.yaml + asserts: + - lengthEqual: + path: spec.template.spec.containers[0].volumeMounts + count: 3 + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: config + mountPath: /config + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: media + mountPath: /media + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: cache + mountPath: /cache + + # Regression: verify all volumes are always present + - it: should always have all three volumes defined + template: deployment.yaml + asserts: + - lengthEqual: + path: spec.template.spec.volumes + count: 3 diff --git a/charts/jellyfin/tests/probes_test.yaml b/charts/jellyfin/tests/probes_test.yaml new file mode 100644 index 0000000..d1a8d45 --- /dev/null +++ b/charts/jellyfin/tests/probes_test.yaml @@ -0,0 +1,447 @@ +suite: test probes (startup, liveness, readiness) +templates: + - deployment.yaml +tests: + # ============================================================================= + # STARTUP PROBE TESTS + # ============================================================================= + + - it: should have startup probe by default + asserts: + - isNotNull: + path: spec.template.spec.containers[0].startupProbe + + - it: should use tcpSocket for startup probe 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 startup probe 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 for startup probe + set: + startupProbe.initialDelaySeconds: 5 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.initialDelaySeconds + value: 5 + + - it: should allow custom periodSeconds for startup probe + set: + startupProbe.periodSeconds: 15 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.periodSeconds + value: 15 + + - it: should allow custom failureThreshold for startup probe + set: + startupProbe.failureThreshold: 60 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.failureThreshold + value: 60 + + - it: should support httpGet for startup 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 exec for startup probe + set: + startupProbe: + exec: + command: ['/bin/sh', '-c', 'curl -f http://localhost:8096/health'] + initialDelaySeconds: 0 + periodSeconds: 10 + failureThreshold: 30 + asserts: + - isNotNull: + path: spec.template.spec.containers[0].startupProbe.exec + - contains: + path: spec.template.spec.containers[0].startupProbe.exec.command + content: '/bin/sh' + + - it: should support custom port for startup probe + set: + startupProbe.tcpSocket.port: 9000 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.tcpSocket.port + value: 9000 + + - it: should calculate correct startup 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) + + # ============================================================================= + # LIVENESS PROBE TESTS + # ============================================================================= + + - it: should have liveness probe by default + asserts: + - isNotNull: + path: spec.template.spec.containers[0].livenessProbe + + - it: should use httpGet for liveness probe by default + asserts: + - isNotNull: + path: spec.template.spec.containers[0].livenessProbe.httpGet + - equal: + path: spec.template.spec.containers[0].livenessProbe.httpGet.path + value: /health + - equal: + path: spec.template.spec.containers[0].livenessProbe.httpGet.port + value: http + + - it: should have default liveness probe initialDelaySeconds + asserts: + - equal: + path: spec.template.spec.containers[0].livenessProbe.initialDelaySeconds + value: 10 + + - it: should allow custom initialDelaySeconds for liveness probe + set: + livenessProbe.initialDelaySeconds: 30 + asserts: + - equal: + path: spec.template.spec.containers[0].livenessProbe.initialDelaySeconds + value: 30 + + - it: should allow custom periodSeconds for liveness probe + set: + livenessProbe.periodSeconds: 20 + asserts: + - equal: + path: spec.template.spec.containers[0].livenessProbe.periodSeconds + value: 20 + + - it: should allow custom failureThreshold for liveness probe + set: + livenessProbe.failureThreshold: 5 + asserts: + - equal: + path: spec.template.spec.containers[0].livenessProbe.failureThreshold + value: 5 + + - it: should allow custom timeoutSeconds for liveness probe + set: + livenessProbe.timeoutSeconds: 5 + asserts: + - equal: + path: spec.template.spec.containers[0].livenessProbe.timeoutSeconds + value: 5 + + - it: should allow custom successThreshold for liveness probe + set: + livenessProbe.successThreshold: 2 + asserts: + - equal: + path: spec.template.spec.containers[0].livenessProbe.successThreshold + value: 2 + + - it: should support tcpSocket for liveness probe + set: + livenessProbe: + tcpSocket: + port: http + initialDelaySeconds: 10 + asserts: + - isNotNull: + path: spec.template.spec.containers[0].livenessProbe.tcpSocket + - equal: + path: spec.template.spec.containers[0].livenessProbe.tcpSocket.port + value: http + + - it: should support exec for liveness probe + set: + livenessProbe: + exec: + command: ['/bin/sh', '-c', 'pgrep jellyfin'] + initialDelaySeconds: 10 + asserts: + - isNotNull: + path: spec.template.spec.containers[0].livenessProbe.exec + - contains: + path: spec.template.spec.containers[0].livenessProbe.exec.command + content: 'pgrep jellyfin' + + - it: should support custom httpGet path for liveness probe + set: + livenessProbe.httpGet.path: /custom/health + asserts: + - equal: + path: spec.template.spec.containers[0].livenessProbe.httpGet.path + value: /custom/health + + - it: should support httpHeaders for liveness probe + set: + livenessProbe: + httpGet: + path: /health + port: http + httpHeaders: + - name: X-Custom-Header + value: CustomValue + initialDelaySeconds: 10 + asserts: + - equal: + path: spec.template.spec.containers[0].livenessProbe.httpGet.httpHeaders[0].name + value: X-Custom-Header + - equal: + path: spec.template.spec.containers[0].livenessProbe.httpGet.httpHeaders[0].value + value: CustomValue + + # ============================================================================= + # READINESS PROBE TESTS + # ============================================================================= + + - it: should have readiness probe by default + asserts: + - isNotNull: + path: spec.template.spec.containers[0].readinessProbe + + - it: should use httpGet for readiness probe by default + asserts: + - isNotNull: + path: spec.template.spec.containers[0].readinessProbe.httpGet + - equal: + path: spec.template.spec.containers[0].readinessProbe.httpGet.path + value: /health + - equal: + path: spec.template.spec.containers[0].readinessProbe.httpGet.port + value: http + + - it: should have default readiness probe initialDelaySeconds + asserts: + - equal: + path: spec.template.spec.containers[0].readinessProbe.initialDelaySeconds + value: 10 + + - it: should allow custom initialDelaySeconds for readiness probe + set: + readinessProbe.initialDelaySeconds: 15 + asserts: + - equal: + path: spec.template.spec.containers[0].readinessProbe.initialDelaySeconds + value: 15 + + - it: should allow custom periodSeconds for readiness probe + set: + readinessProbe.periodSeconds: 15 + asserts: + - equal: + path: spec.template.spec.containers[0].readinessProbe.periodSeconds + value: 15 + + - it: should allow custom failureThreshold for readiness probe + set: + readinessProbe.failureThreshold: 6 + asserts: + - equal: + path: spec.template.spec.containers[0].readinessProbe.failureThreshold + value: 6 + + - it: should allow custom timeoutSeconds for readiness probe + set: + readinessProbe.timeoutSeconds: 3 + asserts: + - equal: + path: spec.template.spec.containers[0].readinessProbe.timeoutSeconds + value: 3 + + - it: should allow custom successThreshold for readiness probe + set: + readinessProbe.successThreshold: 2 + asserts: + - equal: + path: spec.template.spec.containers[0].readinessProbe.successThreshold + value: 2 + + - it: should support tcpSocket for readiness probe + set: + readinessProbe: + tcpSocket: + port: http + initialDelaySeconds: 10 + asserts: + - isNotNull: + path: spec.template.spec.containers[0].readinessProbe.tcpSocket + - equal: + path: spec.template.spec.containers[0].readinessProbe.tcpSocket.port + value: http + + - it: should support exec for readiness probe + set: + readinessProbe: + exec: + command: ['/bin/sh', '-c', 'curl -f http://localhost:8096/health'] + initialDelaySeconds: 10 + asserts: + - isNotNull: + path: spec.template.spec.containers[0].readinessProbe.exec + + - it: should support custom httpGet path for readiness probe + set: + readinessProbe.httpGet.path: /ready + asserts: + - equal: + path: spec.template.spec.containers[0].readinessProbe.httpGet.path + value: /ready + + # ============================================================================= + # EDGE CASES AND COMBINED SCENARIOS + # ============================================================================= + + - it: should allow all three probes with different configurations + set: + startupProbe: + tcpSocket: + port: http + initialDelaySeconds: 0 + periodSeconds: 10 + failureThreshold: 60 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 30 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 15 + periodSeconds: 10 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.failureThreshold + value: 60 + - equal: + path: spec.template.spec.containers[0].livenessProbe.initialDelaySeconds + value: 30 + - equal: + path: spec.template.spec.containers[0].readinessProbe.initialDelaySeconds + value: 15 + + - it: should support all timing parameters for all probes + set: + startupProbe: + tcpSocket: + port: http + initialDelaySeconds: 0 + periodSeconds: 10 + failureThreshold: 30 + timeoutSeconds: 5 + successThreshold: 1 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 20 + periodSeconds: 15 + failureThreshold: 4 + timeoutSeconds: 3 + successThreshold: 1 + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 5 + failureThreshold: 3 + timeoutSeconds: 2 + successThreshold: 2 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.timeoutSeconds + value: 5 + - equal: + path: spec.template.spec.containers[0].livenessProbe.timeoutSeconds + value: 3 + - equal: + path: spec.template.spec.containers[0].readinessProbe.timeoutSeconds + value: 2 + - equal: + path: spec.template.spec.containers[0].readinessProbe.successThreshold + value: 2 + + # Regression: verify all three probes are always present + - it: should always have all three probes configured + asserts: + - isNotNull: + path: spec.template.spec.containers[0].startupProbe + - isNotNull: + path: spec.template.spec.containers[0].livenessProbe + - isNotNull: + path: spec.template.spec.containers[0].readinessProbe + + # Regression: verify default probe types + - it: should use correct probe types by default + asserts: + - isNotNull: + path: spec.template.spec.containers[0].startupProbe.tcpSocket + - isNotNull: + path: spec.template.spec.containers[0].livenessProbe.httpGet + - isNotNull: + path: spec.template.spec.containers[0].readinessProbe.httpGet + + # Different probe types combination + - it: should support different probe types for each probe + set: + startupProbe: + tcpSocket: + port: http + initialDelaySeconds: 0 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + readinessProbe: + exec: + command: ['/bin/sh', '-c', 'test -f /tmp/ready'] + initialDelaySeconds: 5 + asserts: + - isNotNull: + path: spec.template.spec.containers[0].startupProbe.tcpSocket + - isNotNull: + path: spec.template.spec.containers[0].livenessProbe.httpGet + - isNotNull: + path: spec.template.spec.containers[0].readinessProbe.exec diff --git a/charts/jellyfin/tests/regression_defaults_test.yaml b/charts/jellyfin/tests/regression_defaults_test.yaml new file mode 100644 index 0000000..a48ba8f --- /dev/null +++ b/charts/jellyfin/tests/regression_defaults_test.yaml @@ -0,0 +1,564 @@ +suite: test regression defaults +templates: + - deployment.yaml + - service.yaml + - serviceaccount.yaml + - ingress.yaml + - httproute.yaml + - networkpolicy.yaml + - serviceMonitor.yaml + - persistentVolumeClaim.yaml +tests: + # ============================================================================= + # DEPLOYMENT DEFAULTS + # ============================================================================= + + - it: should have default replica count of 1 + template: deployment.yaml + asserts: + - equal: + path: spec.replicas + value: 1 + + - it: should use RollingUpdate strategy by default + template: deployment.yaml + asserts: + - equal: + path: spec.strategy.type + value: RollingUpdate + + - it: should have revisionHistoryLimit of 3 by default + template: deployment.yaml + asserts: + - equal: + path: spec.revisionHistoryLimit + value: 3 + + - it: should use default image jellyfin/jellyfin + template: deployment.yaml + asserts: + - matchRegex: + path: spec.template.spec.containers[0].image + pattern: "^docker\\.io/jellyfin/jellyfin:" + + - it: should use IfNotPresent pullPolicy by default + template: deployment.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].imagePullPolicy + value: IfNotPresent + + - it: should not have resources set by default + template: deployment.yaml + asserts: + - isNull: + path: spec.template.spec.containers[0].resources + + - it: should not set dnsPolicy by default + template: deployment.yaml + asserts: + - isNull: + path: spec.template.spec.dnsPolicy + + - it: should not enable hostNetwork by default + template: deployment.yaml + asserts: + - isNull: + path: spec.template.spec.hostNetwork + + - it: should not have podSecurityContext by default + template: deployment.yaml + asserts: + - isNull: + path: spec.template.spec.securityContext + + - it: should not have container securityContext by default + template: deployment.yaml + asserts: + - isNull: + path: spec.template.spec.containers[0].securityContext + + - it: should expose port 8096 by default + template: deployment.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].ports[0].containerPort + value: 8096 + - equal: + path: spec.template.spec.containers[0].ports[0].name + value: http + + - it: should have exactly 3 default volumes + template: deployment.yaml + asserts: + - lengthEqual: + path: spec.template.spec.volumes + count: 3 + + - it: should have config, media, and cache volumes by default + template: deployment.yaml + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: config + persistentVolumeClaim: + claimName: RELEASE-NAME-jellyfin-config + - contains: + path: spec.template.spec.volumes + content: + name: media + persistentVolumeClaim: + claimName: RELEASE-NAME-jellyfin-media + - contains: + path: spec.template.spec.volumes + content: + name: cache + emptyDir: {} + + - it: should mount all three volumes by default + template: deployment.yaml + asserts: + - lengthEqual: + path: spec.template.spec.containers[0].volumeMounts + count: 3 + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: config + mountPath: /config + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: media + mountPath: /media + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: cache + mountPath: /cache + + # ============================================================================= + # PROBE DEFAULTS + # ============================================================================= + + - it: should have startup probe by default + template: deployment.yaml + asserts: + - isNotNull: + path: spec.template.spec.containers[0].startupProbe + + - it: should use tcpSocket for startup probe by default + template: deployment.yaml + asserts: + - isNotNull: + path: spec.template.spec.containers[0].startupProbe.tcpSocket + + - it: should have default startup probe timing + template: deployment.yaml + 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 have liveness probe by default + template: deployment.yaml + asserts: + - isNotNull: + path: spec.template.spec.containers[0].livenessProbe + + - it: should use httpGet for liveness probe by default + template: deployment.yaml + asserts: + - isNotNull: + path: spec.template.spec.containers[0].livenessProbe.httpGet + - equal: + path: spec.template.spec.containers[0].livenessProbe.httpGet.path + value: /health + + - it: should have default liveness probe initialDelay of 10 + template: deployment.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].livenessProbe.initialDelaySeconds + value: 10 + + - it: should have readiness probe by default + template: deployment.yaml + asserts: + - isNotNull: + path: spec.template.spec.containers[0].readinessProbe + + - it: should use httpGet for readiness probe by default + template: deployment.yaml + asserts: + - isNotNull: + path: spec.template.spec.containers[0].readinessProbe.httpGet + - equal: + path: spec.template.spec.containers[0].readinessProbe.httpGet.path + value: /health + + # ============================================================================= + # SERVICE DEFAULTS + # ============================================================================= + + - it: should create Service by default + template: service.yaml + asserts: + - hasDocuments: + count: 1 + + - it: should use ClusterIP type by default + template: service.yaml + asserts: + - equal: + path: spec.type + value: ClusterIP + + - it: should expose port 8096 by default + template: service.yaml + asserts: + - equal: + path: spec.ports[0].port + value: 8096 + - equal: + path: spec.ports[0].name + value: http + + - it: should not have ipFamilyPolicy set by default + template: service.yaml + asserts: + - isNull: + path: spec.ipFamilyPolicy + + - it: should not have ipFamilies set by default + template: service.yaml + asserts: + - isNull: + path: spec.ipFamilies + + # ============================================================================= + # SERVICEACCOUNT DEFAULTS + # ============================================================================= + + - it: should create ServiceAccount by default + template: serviceaccount.yaml + asserts: + - hasDocuments: + count: 1 + + - it: should enable automount by default + template: deployment.yaml + asserts: + - equal: + path: spec.template.spec.automountServiceAccountToken + value: true + + # ============================================================================= + # PERSISTENCE DEFAULTS + # ============================================================================= + + - it: should create config and media PVCs by default + template: persistentVolumeClaim.yaml + asserts: + - hasDocuments: + count: 2 + + - it: should have config persistence enabled with size 5Gi by default + template: persistentVolumeClaim.yaml + documentIndex: 0 + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-jellyfin-config + - equal: + path: spec.resources.requests.storage + value: 5Gi + + - it: should have media persistence enabled with size 25Gi by default + template: persistentVolumeClaim.yaml + documentIndex: 1 + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-jellyfin-media + - equal: + path: spec.resources.requests.storage + value: 25Gi + + - it: should use ReadWriteOnce accessMode by default + template: persistentVolumeClaim.yaml + documentIndex: 0 + asserts: + - contains: + path: spec.accessModes + content: ReadWriteOnce + + - it: should not create cache PVC by default + template: persistentVolumeClaim.yaml + asserts: + - hasDocuments: + count: 2 + + - it: should use emptyDir for cache by default + template: deployment.yaml + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: cache + emptyDir: {} + + # ============================================================================= + # OPTIONAL FEATURES DISABLED BY DEFAULT + # ============================================================================= + + - it: should not create Ingress by default + template: ingress.yaml + asserts: + - hasDocuments: + count: 0 + + - it: should not create HTTPRoute by default + template: httproute.yaml + asserts: + - hasDocuments: + count: 0 + + - it: should not create NetworkPolicy by default + template: networkpolicy.yaml + asserts: + - hasDocuments: + count: 0 + + - it: should not create ServiceMonitor by default + template: serviceMonitor.yaml + asserts: + - hasDocuments: + count: 0 + + - it: should not enable DLNA by default + template: deployment.yaml + asserts: + - isNull: + path: spec.template.spec.hostNetwork + + - it: should not enable metrics by default + template: deployment.yaml + asserts: + - isNull: + path: spec.template.spec.containers[0].lifecycle + + - it: should not have init containers by default + template: deployment.yaml + asserts: + - isNull: + path: spec.template.spec.initContainers + + - it: should not have extra containers by default + template: deployment.yaml + asserts: + - lengthEqual: + path: spec.template.spec.containers + count: 1 + + - it: should not have imagePullSecrets by default + template: deployment.yaml + asserts: + - isNull: + path: spec.template.spec.imagePullSecrets + + # ============================================================================= + # LABEL DEFAULTS + # ============================================================================= + + - it: should have standard Kubernetes labels on deployment + template: deployment.yaml + asserts: + - equal: + path: metadata.labels["app.kubernetes.io/name"] + value: jellyfin + - equal: + path: metadata.labels["app.kubernetes.io/instance"] + value: RELEASE-NAME + - isNotNull: + path: metadata.labels["app.kubernetes.io/version"] + - isNotNull: + path: metadata.labels["helm.sh/chart"] + + - it: should have standard labels on service + template: service.yaml + asserts: + - equal: + path: metadata.labels["app.kubernetes.io/name"] + value: jellyfin + - equal: + path: metadata.labels["app.kubernetes.io/instance"] + value: RELEASE-NAME + + - it: should have standard labels on serviceaccount + template: serviceaccount.yaml + asserts: + - equal: + path: metadata.labels["app.kubernetes.io/name"] + value: jellyfin + - equal: + path: metadata.labels["app.kubernetes.io/instance"] + value: RELEASE-NAME + + - it: should have standard labels on PVCs + template: persistentVolumeClaim.yaml + documentIndex: 0 + asserts: + - equal: + path: metadata.labels["app.kubernetes.io/name"] + value: jellyfin + - equal: + path: metadata.labels["app.kubernetes.io/instance"] + value: RELEASE-NAME + + # ============================================================================= + # API VERSIONS + # ============================================================================= + + - it: should use apps/v1 for Deployment + template: deployment.yaml + asserts: + - equal: + path: apiVersion + value: apps/v1 + + - it: should use v1 for Service + template: service.yaml + asserts: + - equal: + path: apiVersion + value: v1 + + - it: should use v1 for ServiceAccount + template: serviceaccount.yaml + asserts: + - equal: + path: apiVersion + value: v1 + + - it: should use v1 for PersistentVolumeClaim + template: persistentVolumeClaim.yaml + documentIndex: 0 + asserts: + - equal: + path: apiVersion + value: v1 + + # ============================================================================= + # REGRESSION: VERIFY NO UNEXPECTED CHANGES + # ============================================================================= + + - it: should maintain exactly one container by default + template: deployment.yaml + asserts: + - lengthEqual: + path: spec.template.spec.containers + count: 1 + + - it: should maintain container name as jellyfin + template: deployment.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].name + value: jellyfin + + - it: should maintain service name pattern + template: service.yaml + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-jellyfin + + - it: should maintain serviceAccount name pattern + template: serviceaccount.yaml + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-jellyfin + + - it: should maintain deployment name pattern + template: deployment.yaml + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-jellyfin + + - it: should maintain PVC naming pattern (config) + template: persistentVolumeClaim.yaml + documentIndex: 0 + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-jellyfin-config + + - it: should maintain PVC naming pattern (media) + template: persistentVolumeClaim.yaml + documentIndex: 1 + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-jellyfin-media + + # ============================================================================= + # CRITICAL DEFAULTS THAT MUST NOT CHANGE + # ============================================================================= + + - it: should never create multiple replicas by default + template: deployment.yaml + asserts: + - equal: + path: spec.replicas + value: 1 + + - it: should never enable hostNetwork by default + template: deployment.yaml + asserts: + - isNull: + path: spec.template.spec.hostNetwork + + - it: should never enable NetworkPolicy by default + template: networkpolicy.yaml + asserts: + - hasDocuments: + count: 0 + + - it: should always create exactly one service + template: service.yaml + asserts: + - hasDocuments: + count: 1 + + - it: should always create serviceAccount by default + template: serviceaccount.yaml + asserts: + - hasDocuments: + count: 1 + + - it: should always have three probes configured + template: deployment.yaml + asserts: + - isNotNull: + path: spec.template.spec.containers[0].startupProbe + - isNotNull: + path: spec.template.spec.containers[0].livenessProbe + - isNotNull: + path: spec.template.spec.containers[0].readinessProbe + + - it: should always mount all persistence volumes + template: deployment.yaml + asserts: + - lengthEqual: + path: spec.template.spec.containers[0].volumeMounts + count: 3 diff --git a/charts/jellyfin/tests/service_ipv6_test.yaml b/charts/jellyfin/tests/service_ipv6_test.yaml deleted file mode 100644 index 5dee731..0000000 --- a/charts/jellyfin/tests/service_ipv6_test.yaml +++ /dev/null @@ -1,128 +0,0 @@ -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/service_test.yaml b/charts/jellyfin/tests/service_test.yaml new file mode 100644 index 0000000..07e1204 --- /dev/null +++ b/charts/jellyfin/tests/service_test.yaml @@ -0,0 +1,440 @@ +suite: test service +templates: + - service.yaml +tests: + # ============================================================================= + # BASIC SERVICE DEFAULTS + # ============================================================================= + + - it: should create Service by default + asserts: + - hasDocuments: + count: 1 + - isKind: + of: Service + - isAPIVersion: + of: v1 + + - it: should have correct default name + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-jellyfin + + - it: should use ClusterIP type by default + asserts: + - equal: + path: spec.type + value: ClusterIP + + - it: should expose port 8096 by default + asserts: + - equal: + path: spec.ports[0].port + value: 8096 + - equal: + path: spec.ports[0].targetPort + value: http + - equal: + path: spec.ports[0].protocol + value: TCP + - equal: + path: spec.ports[0].name + value: http + + - it: should have correct pod selector labels + asserts: + - equal: + path: spec.selector["app.kubernetes.io/name"] + value: jellyfin + - equal: + path: spec.selector["app.kubernetes.io/instance"] + value: RELEASE-NAME + + # ============================================================================= + # SERVICE TYPE TESTS + # ============================================================================= + + - it: should support ClusterIP type explicitly + set: + service.type: ClusterIP + asserts: + - equal: + path: spec.type + value: ClusterIP + - isNull: + path: spec.clusterIP + + - it: should support NodePort type + set: + service.type: NodePort + asserts: + - equal: + path: spec.type + value: NodePort + + - it: should support LoadBalancer type + set: + service.type: LoadBalancer + asserts: + - equal: + path: spec.type + value: LoadBalancer + + # ============================================================================= + # PORT CONFIGURATION + # ============================================================================= + + - it: should set custom port + set: + service.port: 9096 + asserts: + - equal: + path: spec.ports[0].port + value: 9096 + + - it: should set custom nodePort when type is NodePort + set: + service.type: NodePort + service.nodePort: 30096 + asserts: + - equal: + path: spec.ports[0].nodePort + value: 30096 + + - it: should not set nodePort when type is ClusterIP + set: + service.type: ClusterIP + service.nodePort: 30096 + asserts: + - isNull: + path: spec.ports[0].nodePort + + # Port range validation would be done by values.schema.json + # Here we just test that values are applied correctly + - it: should accept nodePort in valid range (30000-32767) + set: + service.type: NodePort + service.nodePort: 30000 + asserts: + - equal: + path: spec.ports[0].nodePort + value: 30000 + + - it: should accept nodePort at upper range + set: + service.type: NodePort + service.nodePort: 32767 + asserts: + - equal: + path: spec.ports[0].nodePort + value: 32767 + + # ============================================================================= + # LOADBALANCER CONFIGURATION + # ============================================================================= + + - it: should set loadBalancerIP when specified + set: + service.type: LoadBalancer + service.loadBalancerIP: 203.0.113.42 + asserts: + - equal: + path: spec.loadBalancerIP + value: 203.0.113.42 + + - it: should set loadBalancerClass when specified + set: + service.type: LoadBalancer + service.loadBalancerClass: metallb + asserts: + - equal: + path: spec.loadBalancerClass + value: metallb + + - it: should set loadBalancerSourceRanges when specified + set: + service.type: LoadBalancer + service.loadBalancerSourceRanges: + - 10.0.0.0/8 + - 192.168.0.0/16 + asserts: + - contains: + path: spec.loadBalancerSourceRanges + content: 10.0.0.0/8 + - contains: + path: spec.loadBalancerSourceRanges + content: 192.168.0.0/16 + + - it: should not set loadBalancerIP when type is ClusterIP + set: + service.type: ClusterIP + service.loadBalancerIP: 203.0.113.42 + asserts: + - isNull: + path: spec.loadBalancerIP + + - it: should set all loadBalancer options together + set: + service.type: LoadBalancer + service.loadBalancerIP: 203.0.113.42 + service.loadBalancerClass: metallb + service.loadBalancerSourceRanges: + - 10.0.0.0/8 + asserts: + - equal: + path: spec.loadBalancerIP + value: 203.0.113.42 + - equal: + path: spec.loadBalancerClass + value: metallb + - contains: + path: spec.loadBalancerSourceRanges + content: 10.0.0.0/8 + + # ============================================================================= + # ANNOTATIONS + # ============================================================================= + + - it: should not have annotations by default + asserts: + - isNull: + path: metadata.annotations + + - it: should add custom annotations + set: + service.annotations: + service.beta.kubernetes.io/aws-load-balancer-type: nlb + external-dns.alpha.kubernetes.io/hostname: jellyfin.example.com + asserts: + - equal: + path: metadata.annotations["service.beta.kubernetes.io/aws-load-balancer-type"] + value: nlb + - equal: + path: metadata.annotations["external-dns.alpha.kubernetes.io/hostname"] + value: jellyfin.example.com + + - it: should support empty annotations object + set: + service.annotations: {} + asserts: + - isNull: + path: metadata.annotations + + # ============================================================================= + # IPv4/IPv6 DUAL-STACK 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 + + # ============================================================================= + # EDGE CASES AND VALIDATIONS + # ============================================================================= + + # LoadBalancer without explicit IP (valid use case) + - it: should create LoadBalancer without loadBalancerIP + set: + service.type: LoadBalancer + asserts: + - equal: + path: spec.type + value: LoadBalancer + - isNull: + path: spec.loadBalancerIP + + # NodePort without explicit nodePort (auto-assign) + - it: should create NodePort without explicit nodePort + set: + service.type: NodePort + asserts: + - equal: + path: spec.type + value: NodePort + - isNull: + path: spec.ports[0].nodePort + + # Multiple loadBalancerSourceRanges + - it: should support multiple loadBalancerSourceRanges + set: + service.type: LoadBalancer + service.loadBalancerSourceRanges: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + asserts: + - lengthEqual: + path: spec.loadBalancerSourceRanges + count: 3 + + # Regression: verify service always has one port + - it: should always have exactly one port defined + asserts: + - lengthEqual: + path: spec.ports + count: 1 + + # Regression: verify port name is always http + - it: should always name port as http + set: + service.port: 9096 + asserts: + - equal: + path: spec.ports[0].name + value: http + - equal: + path: spec.ports[0].targetPort + value: http + + # Combining multiple settings + - it: should combine custom port with NodePort and annotations + set: + service.type: NodePort + service.port: 9096 + service.nodePort: 31096 + service.annotations: + custom: annotation + asserts: + - equal: + path: spec.type + value: NodePort + - equal: + path: spec.ports[0].port + value: 9096 + - equal: + path: spec.ports[0].nodePort + value: 31096 + - equal: + path: metadata.annotations.custom + value: annotation + + # Regression: verify protocol is always TCP + - it: should always use TCP protocol + set: + service.port: 9096 + asserts: + - equal: + path: spec.ports[0].protocol + value: TCP diff --git a/charts/jellyfin/tests/serviceaccount_test.yaml b/charts/jellyfin/tests/serviceaccount_test.yaml new file mode 100644 index 0000000..b7bb239 --- /dev/null +++ b/charts/jellyfin/tests/serviceaccount_test.yaml @@ -0,0 +1,355 @@ +suite: test serviceaccount +templates: + - serviceaccount.yaml + - deployment.yaml +tests: + # ============================================================================= + # DEFAULT BEHAVIOR + # ============================================================================= + + - it: should create ServiceAccount by default + template: serviceaccount.yaml + asserts: + - hasDocuments: + count: 1 + - isKind: + of: ServiceAccount + - isAPIVersion: + of: v1 + + - it: should have auto-generated name by default + template: serviceaccount.yaml + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-jellyfin + + - it: should use ServiceAccount in deployment by default + template: deployment.yaml + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: RELEASE-NAME-jellyfin + + - it: should automount service account token by default + template: deployment.yaml + asserts: + - equal: + path: spec.template.spec.automountServiceAccountToken + value: true + + # ============================================================================= + # CREATE CONFIGURATION + # ============================================================================= + + - it: should not create ServiceAccount when create is false + template: serviceaccount.yaml + set: + serviceAccount.create: false + asserts: + - hasDocuments: + count: 0 + + - it: should use default ServiceAccount when create is false + template: deployment.yaml + set: + serviceAccount.create: false + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: default + + - it: should create ServiceAccount when explicitly enabled + template: serviceaccount.yaml + set: + serviceAccount.create: true + asserts: + - hasDocuments: + count: 1 + + # ============================================================================= + # CUSTOM NAME + # ============================================================================= + + - it: should use custom name when specified + template: serviceaccount.yaml + set: + serviceAccount.name: my-custom-sa + asserts: + - equal: + path: metadata.name + value: my-custom-sa + + - it: should use custom name in deployment + template: deployment.yaml + set: + serviceAccount.name: my-custom-sa + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: my-custom-sa + + - it: should use custom name when create is false + template: deployment.yaml + set: + serviceAccount.create: false + serviceAccount.name: existing-sa + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: existing-sa + + - it: should not create SA but use custom name when create is false + template: serviceaccount.yaml + set: + serviceAccount.create: false + serviceAccount.name: existing-external-sa + asserts: + - hasDocuments: + count: 0 + + # ============================================================================= + # ANNOTATIONS + # ============================================================================= + + - it: should not have annotations by default + template: serviceaccount.yaml + asserts: + - isNull: + path: metadata.annotations + + - it: should add custom annotations + template: serviceaccount.yaml + set: + serviceAccount.annotations: + eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/jellyfin-role + asserts: + - equal: + path: metadata.annotations["eks.amazonaws.com/role-arn"] + value: arn:aws:iam::123456789012:role/jellyfin-role + + - it: should support multiple annotations + template: serviceaccount.yaml + set: + serviceAccount.annotations: + annotation1: value1 + annotation2: value2 + annotation3: value3 + asserts: + - equal: + path: metadata.annotations.annotation1 + value: value1 + - equal: + path: metadata.annotations.annotation2 + value: value2 + - equal: + path: metadata.annotations.annotation3 + value: value3 + + - it: should support GCP Workload Identity annotation + template: serviceaccount.yaml + set: + serviceAccount.annotations: + iam.gke.io/gcp-service-account: jellyfin@project-id.iam.gserviceaccount.com + asserts: + - equal: + path: metadata.annotations["iam.gke.io/gcp-service-account"] + value: jellyfin@project-id.iam.gserviceaccount.com + + - it: should support Azure Workload Identity annotation + template: serviceaccount.yaml + set: + serviceAccount.annotations: + azure.workload.identity/client-id: 12345678-1234-1234-1234-123456789012 + asserts: + - equal: + path: metadata.annotations["azure.workload.identity/client-id"] + value: 12345678-1234-1234-1234-123456789012 + + # ============================================================================= + # AUTOMOUNT CONFIGURATION + # ============================================================================= + + - it: should enable automount by default + template: deployment.yaml + asserts: + - equal: + path: spec.template.spec.automountServiceAccountToken + value: true + + - it: should disable automount when set to false + template: deployment.yaml + set: + serviceAccount.automount: false + asserts: + - equal: + path: spec.template.spec.automountServiceAccountToken + value: false + + - it: should explicitly enable automount when set to true + template: deployment.yaml + set: + serviceAccount.automount: true + asserts: + - equal: + path: spec.template.spec.automountServiceAccountToken + value: true + + # ============================================================================= + # EDGE CASES + # ============================================================================= + + - it: should handle empty name gracefully (use auto-generated) + template: serviceaccount.yaml + set: + serviceAccount.name: "" + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-jellyfin + + - it: should handle empty annotations object + template: serviceaccount.yaml + set: + serviceAccount.annotations: {} + asserts: + - isNull: + path: metadata.annotations + + - it: should work with custom name and annotations together + template: serviceaccount.yaml + set: + serviceAccount.name: custom-sa + serviceAccount.annotations: + custom: annotation + asserts: + - equal: + path: metadata.name + value: custom-sa + - equal: + path: metadata.annotations.custom + value: annotation + + - it: should work with all options combined + template: serviceaccount.yaml + set: + serviceAccount.create: true + serviceAccount.name: jellyfin-prod-sa + serviceAccount.annotations: + eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/jellyfin + custom.io/annotation: value + asserts: + - equal: + path: metadata.name + value: jellyfin-prod-sa + - equal: + path: metadata.annotations["eks.amazonaws.com/role-arn"] + value: arn:aws:iam::123456789012:role/jellyfin + - equal: + path: metadata.annotations["custom.io/annotation"] + value: value + + # ============================================================================= + # REGRESSION TESTS + # ============================================================================= + + - it: should always have correct labels + template: serviceaccount.yaml + asserts: + - equal: + path: metadata.labels["app.kubernetes.io/name"] + value: jellyfin + - equal: + path: metadata.labels["app.kubernetes.io/instance"] + value: RELEASE-NAME + + - it: should always use v1 API version + template: serviceaccount.yaml + asserts: + - equal: + path: apiVersion + value: v1 + + - it: should always be ServiceAccount kind + template: serviceaccount.yaml + asserts: + - equal: + path: kind + value: ServiceAccount + + # Verify deployment always sets serviceAccountName + - it: should always set serviceAccountName in deployment + template: deployment.yaml + asserts: + - isNotNull: + path: spec.template.spec.serviceAccountName + + - it: should always set automountServiceAccountToken in deployment + template: deployment.yaml + asserts: + - isNotNull: + path: spec.template.spec.automountServiceAccountToken + + # ============================================================================= + # INTEGRATION SCENARIOS + # ============================================================================= + + # Scenario: Using existing external ServiceAccount + - it: should use existing external ServiceAccount without creating new one + set: + serviceAccount.create: false + serviceAccount.name: external-jellyfin-sa + serviceAccount.automount: false + asserts: + - template: serviceaccount.yaml + hasDocuments: + count: 0 + - template: deployment.yaml + equal: + path: spec.template.spec.serviceAccountName + value: external-jellyfin-sa + - template: deployment.yaml + equal: + path: spec.template.spec.automountServiceAccountToken + value: false + + # Scenario: AWS IRSA (IAM Roles for Service Accounts) + - it: should configure for AWS IRSA + template: serviceaccount.yaml + set: + serviceAccount.create: true + serviceAccount.name: jellyfin-irsa + serviceAccount.annotations: + eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/jellyfin-s3-access + asserts: + - equal: + path: metadata.name + value: jellyfin-irsa + - equal: + path: metadata.annotations["eks.amazonaws.com/role-arn"] + value: arn:aws:iam::123456789012:role/jellyfin-s3-access + + # Scenario: GCP Workload Identity + - it: should configure for GCP Workload Identity + template: serviceaccount.yaml + set: + serviceAccount.annotations: + iam.gke.io/gcp-service-account: jellyfin@my-project.iam.gserviceaccount.com + asserts: + - equal: + path: metadata.annotations["iam.gke.io/gcp-service-account"] + value: jellyfin@my-project.iam.gserviceaccount.com + + # Scenario: Minimal security (no automount) + - it: should support minimal security setup without token automount + set: + serviceAccount.create: true + serviceAccount.automount: false + asserts: + - template: serviceaccount.yaml + hasDocuments: + count: 1 + - template: deployment.yaml + equal: + path: spec.template.spec.automountServiceAccountToken + value: false diff --git a/charts/jellyfin/tests/servicemonitor_test.yaml b/charts/jellyfin/tests/servicemonitor_test.yaml new file mode 100644 index 0000000..29c87a3 --- /dev/null +++ b/charts/jellyfin/tests/servicemonitor_test.yaml @@ -0,0 +1,482 @@ +suite: test servicemonitor +templates: + - serviceMonitor.yaml +tests: + # ============================================================================= + # DEFAULT BEHAVIOR + # ============================================================================= + + - it: should not create ServiceMonitor by default + asserts: + - hasDocuments: + count: 0 + + - it: should not create ServiceMonitor when only metrics.enabled + set: + metrics.enabled: true + asserts: + - hasDocuments: + count: 0 + + - it: should not create ServiceMonitor when only serviceMonitor.enabled + set: + metrics.serviceMonitor.enabled: true + asserts: + - hasDocuments: + count: 0 + + - it: should create ServiceMonitor when both metrics and serviceMonitor enabled + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + asserts: + - hasDocuments: + count: 1 + - isKind: + of: ServiceMonitor + - isAPIVersion: + of: monitoring.coreos.com/v1 + + # ============================================================================= + # BASIC CONFIGURATION + # ============================================================================= + + - it: should have correct default name + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-jellyfin + + - it: should not set namespace by default + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + asserts: + - isNull: + path: metadata.namespace + + - it: should use custom namespace when specified + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.namespace: monitoring + asserts: + - equal: + path: metadata.namespace + value: monitoring + + - it: should have correct selector labels + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + asserts: + - equal: + path: spec.selector.matchLabels["app.kubernetes.io/name"] + value: jellyfin + - equal: + path: spec.selector.matchLabels["app.kubernetes.io/instance"] + value: RELEASE-NAME + + # ============================================================================= + # LABELS + # ============================================================================= + + - it: should not have custom labels by default + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + asserts: + - equal: + path: metadata.labels["app.kubernetes.io/name"] + value: jellyfin + + - it: should add custom labels when specified + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.labels: + prometheus: kube-prometheus + team: platform + asserts: + - equal: + path: metadata.labels.prometheus + value: kube-prometheus + - equal: + path: metadata.labels.team + value: platform + + - it: should merge custom labels with default labels + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.labels: + custom: label + asserts: + - equal: + path: metadata.labels["app.kubernetes.io/name"] + value: jellyfin + - equal: + path: metadata.labels.custom + value: label + + # ============================================================================= + # ENDPOINT CONFIGURATION + # ============================================================================= + + - it: should have default endpoint configuration + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + asserts: + - lengthEqual: + path: spec.endpoints + count: 1 + - equal: + path: spec.endpoints[0].port + value: http + - equal: + path: spec.endpoints[0].path + value: /metrics + - equal: + path: spec.endpoints[0].scheme + value: http + - equal: + path: spec.endpoints[0].interval + value: 30s + - equal: + path: spec.endpoints[0].scrapeTimeout + value: 30s + + - it: should set custom interval + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.interval: 60s + asserts: + - equal: + path: spec.endpoints[0].interval + value: 60s + + - it: should set custom scrapeTimeout + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.scrapeTimeout: 15s + asserts: + - equal: + path: spec.endpoints[0].scrapeTimeout + value: 15s + + - it: should set custom path + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.path: /custom/metrics + asserts: + - equal: + path: spec.endpoints[0].path + value: /custom/metrics + + - it: should set custom scheme + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.scheme: https + asserts: + - equal: + path: spec.endpoints[0].scheme + value: https + + # ============================================================================= + # TLS CONFIGURATION + # ============================================================================= + + - it: should not have tlsConfig by default + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + asserts: + - isNull: + path: spec.endpoints[0].tlsConfig + + - it: should add tlsConfig when specified + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.tlsConfig: + insecureSkipVerify: true + asserts: + - equal: + path: spec.endpoints[0].tlsConfig.insecureSkipVerify + value: true + + - it: should support complex tlsConfig + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.tlsConfig: + ca: + secret: + name: prometheus-tls-ca + key: ca.crt + cert: + secret: + name: prometheus-tls-cert + key: tls.crt + keySecret: + name: prometheus-tls-key + key: tls.key + asserts: + - equal: + path: spec.endpoints[0].tlsConfig.ca.secret.name + value: prometheus-tls-ca + - equal: + path: spec.endpoints[0].tlsConfig.cert.secret.name + value: prometheus-tls-cert + + # ============================================================================= + # RELABELING + # ============================================================================= + + - it: should not have relabelings by default + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + asserts: + - isNull: + path: spec.endpoints[0].relabelings + + - it: should add relabelings when specified + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.relabelings: + - sourceLabels: [__meta_kubernetes_pod_name] + targetLabel: pod + asserts: + - lengthEqual: + path: spec.endpoints[0].relabelings + count: 1 + - contains: + path: spec.endpoints[0].relabelings[0].sourceLabels + content: __meta_kubernetes_pod_name + - equal: + path: spec.endpoints[0].relabelings[0].targetLabel + value: pod + + - it: should support multiple relabelings + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.relabelings: + - sourceLabels: [__meta_kubernetes_pod_name] + targetLabel: pod + - sourceLabels: [__meta_kubernetes_namespace] + targetLabel: namespace + - action: drop + regex: temp.* + sourceLabels: [__meta_kubernetes_pod_label_temp] + asserts: + - lengthEqual: + path: spec.endpoints[0].relabelings + count: 3 + + - it: should not have metricRelabelings by default + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + asserts: + - isNull: + path: spec.endpoints[0].metricRelabelings + + - it: should add metricRelabelings when specified + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.metricRelabelings: + - sourceLabels: [__name__] + regex: 'http_.*' + action: keep + asserts: + - lengthEqual: + path: spec.endpoints[0].metricRelabelings + count: 1 + - contains: + path: spec.endpoints[0].metricRelabelings[0].sourceLabels + content: __name__ + - equal: + path: spec.endpoints[0].metricRelabelings[0].action + value: keep + + # ============================================================================= + # TARGET LABELS + # ============================================================================= + + - it: should not have targetLabels by default + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + asserts: + - isNull: + path: spec.targetLabels + + - it: should add targetLabels when specified + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.targetLabels: + - app + - version + asserts: + - lengthEqual: + path: spec.targetLabels + count: 2 + - contains: + path: spec.targetLabels + content: app + - contains: + path: spec.targetLabels + content: version + + # ============================================================================= + # EDGE CASES + # ============================================================================= + + # Interval format validation (these should work with Prometheus) + - it: should accept various interval formats + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.interval: 1m + asserts: + - equal: + path: spec.endpoints[0].interval + value: 1m + + - it: should accept interval in hours + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.interval: 1h + asserts: + - equal: + path: spec.endpoints[0].interval + value: 1h + + # Empty configurations + - it: should handle empty labels object + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.labels: {} + asserts: + - equal: + path: metadata.labels["app.kubernetes.io/name"] + value: jellyfin + + - it: should handle empty relabelings array + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.relabelings: [] + asserts: + - isNull: + path: spec.endpoints[0].relabelings + + # ============================================================================= + # COMPLETE SCENARIOS + # ============================================================================= + + - it: should create complete ServiceMonitor with all features + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.namespace: monitoring + metrics.serviceMonitor.labels: + prometheus: kube-prometheus + metrics.serviceMonitor.interval: 1m + metrics.serviceMonitor.scrapeTimeout: 30s + metrics.serviceMonitor.path: /metrics + metrics.serviceMonitor.port: 8096 + metrics.serviceMonitor.scheme: https + metrics.serviceMonitor.tlsConfig: + insecureSkipVerify: true + metrics.serviceMonitor.relabelings: + - sourceLabels: [__meta_kubernetes_pod_name] + targetLabel: pod + metrics.serviceMonitor.metricRelabelings: + - action: drop + regex: 'unnecessary_metric_.*' + sourceLabels: [__name__] + metrics.serviceMonitor.targetLabels: + - app + asserts: + - equal: + path: metadata.namespace + value: monitoring + - equal: + path: metadata.labels.prometheus + value: kube-prometheus + - equal: + path: spec.endpoints[0].interval + value: 1m + - equal: + path: spec.endpoints[0].scheme + value: https + - isNotNull: + path: spec.endpoints[0].tlsConfig + - lengthEqual: + path: spec.endpoints[0].relabelings + count: 1 + - lengthEqual: + path: spec.endpoints[0].metricRelabelings + count: 1 + - lengthEqual: + path: spec.targetLabels + count: 1 + + # ============================================================================= + # REGRESSION TESTS + # ============================================================================= + + - it: should always use monitoring.coreos.com/v1 API version + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + asserts: + - equal: + path: apiVersion + value: monitoring.coreos.com/v1 + + - it: should always have correct chart labels + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + asserts: + - equal: + path: metadata.labels["app.kubernetes.io/name"] + value: jellyfin + - equal: + path: metadata.labels["app.kubernetes.io/instance"] + value: RELEASE-NAME + + - it: should always have exactly one endpoint + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + asserts: + - lengthEqual: + path: spec.endpoints + count: 1 + + # Verify dependency on metrics.enabled + - it: should require both metrics.enabled and serviceMonitor.enabled + set: + metrics.enabled: false + metrics.serviceMonitor.enabled: true + asserts: + - hasDocuments: + count: 0 diff --git a/charts/jellyfin/tests/startup_probe_test.yaml b/charts/jellyfin/tests/startup_probe_test.yaml deleted file mode 100644 index ee7430f..0000000 --- a/charts/jellyfin/tests/startup_probe_test.yaml +++ /dev/null @@ -1,92 +0,0 @@ -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/tests/validation_mutual_exclusions_test.yaml b/charts/jellyfin/tests/validation_mutual_exclusions_test.yaml new file mode 100644 index 0000000..3aeabb8 --- /dev/null +++ b/charts/jellyfin/tests/validation_mutual_exclusions_test.yaml @@ -0,0 +1,477 @@ +suite: test validation and mutual exclusions +templates: + - deployment.yaml + - networkpolicy.yaml + - serviceMonitor.yaml + - service.yaml + - ingress.yaml + - httproute.yaml + - persistentVolumeClaim.yaml +tests: + # ============================================================================= + # NETWORKPOLICY MUTUAL EXCLUSIONS + # ============================================================================= + + - it: should fail when NetworkPolicy enabled with DLNA + template: networkpolicy.yaml + set: + networkPolicy.enabled: true + jellyfin.enableDLNA: true + asserts: + - failedTemplate: {} + + - it: should fail when NetworkPolicy enabled with hostNetwork + template: networkpolicy.yaml + set: + networkPolicy.enabled: true + podPrivileges.hostNetwork: true + asserts: + - failedTemplate: {} + + - it: should fail when NetworkPolicy enabled with both DLNA and hostNetwork + template: networkpolicy.yaml + set: + networkPolicy.enabled: true + jellyfin.enableDLNA: true + podPrivileges.hostNetwork: true + asserts: + - failedTemplate: {} + + - it: should succeed when NetworkPolicy enabled without DLNA or hostNetwork + template: networkpolicy.yaml + set: + networkPolicy.enabled: true + jellyfin.enableDLNA: false + podPrivileges.hostNetwork: false + asserts: + - hasDocuments: + count: 1 + + - it: should succeed when NetworkPolicy disabled with DLNA + template: networkpolicy.yaml + set: + networkPolicy.enabled: false + jellyfin.enableDLNA: true + asserts: + - hasDocuments: + count: 0 + + # ============================================================================= + # SERVICEMONITOR DEPENDENCIES + # ============================================================================= + + - it: should not create ServiceMonitor when only metrics.enabled + template: serviceMonitor.yaml + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: false + asserts: + - hasDocuments: + count: 0 + + - it: should not create ServiceMonitor when only serviceMonitor.enabled + template: serviceMonitor.yaml + set: + metrics.enabled: false + metrics.serviceMonitor.enabled: true + asserts: + - hasDocuments: + count: 0 + + - it: should create ServiceMonitor when both enabled + template: serviceMonitor.yaml + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + asserts: + - hasDocuments: + count: 1 + + # ============================================================================= + # DLNA AND HOSTNETWORK SCENARIOS + # ============================================================================= + + - it: should enable hostNetwork automatically when DLNA enabled + template: deployment.yaml + set: + jellyfin.enableDLNA: true + asserts: + - equal: + path: spec.template.spec.hostNetwork + value: true + - equal: + path: spec.template.spec.dnsPolicy + value: ClusterFirstWithHostNet + + - it: should not enable hostNetwork when DLNA disabled + template: deployment.yaml + set: + jellyfin.enableDLNA: false + asserts: + - isNull: + path: spec.template.spec.hostNetwork + + - it: should allow hostNetwork without DLNA + template: deployment.yaml + set: + jellyfin.enableDLNA: false + podPrivileges.hostNetwork: true + asserts: + - equal: + path: spec.template.spec.hostNetwork + value: true + + # ============================================================================= + # REPLICACOUNT VALIDATION (WARNING, NOT ERROR) + # ============================================================================= + + # Note: Jellyfin doesn't support horizontal scaling, but chart allows it + # Users should be warned but not prevented from setting replicaCount > 1 + + - it: should allow replicaCount of 1 (recommended) + template: deployment.yaml + set: + replicaCount: 1 + asserts: + - equal: + path: spec.replicas + value: 1 + + - it: should allow replicaCount > 1 (not recommended but not forbidden) + template: deployment.yaml + set: + replicaCount: 3 + asserts: + - equal: + path: spec.replicas + value: 3 + + # ============================================================================= + # PERSISTENCE VALIDATIONS + # ============================================================================= + + # These are handled by values.schema.json, testing that templates work correctly + + - it: should handle persistence.media.type=pvc without errors + template: deployment.yaml + set: + persistence.media.type: pvc + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: media + persistentVolumeClaim: + claimName: RELEASE-NAME-jellyfin-media + + - it: should handle persistence.media.type=hostPath with path + template: deployment.yaml + set: + persistence.media.type: hostPath + persistence.media.hostPath: /mnt/media + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: media + hostPath: + path: /mnt/media + type: Directory + + - it: should handle persistence.media.type=emptyDir + template: deployment.yaml + set: + persistence.media.type: emptyDir + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: media + emptyDir: {} + + # existingClaim should not create PVC but should mount it + - it: should not create PVC when existingClaim is used + template: persistentVolumeClaim.yaml + set: + persistence.media.type: pvc + persistence.media.existingClaim: my-existing-claim + asserts: + - hasDocuments: + count: 1 # only config, not media + + - it: should mount existingClaim in deployment + template: deployment.yaml + set: + persistence.media.type: pvc + persistence.media.existingClaim: my-existing-claim + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: media + persistentVolumeClaim: + claimName: my-existing-claim + + # ============================================================================= + # INGRESS VALIDATIONS + # ============================================================================= + + - it: should allow ingress with empty hosts (edge case, likely misconfiguration) + template: ingress.yaml + set: + ingress.enabled: true + ingress.hosts: [] + asserts: + - hasDocuments: + count: 1 + - lengthEqual: + path: spec.rules + count: 0 + + - it: should allow ingress with hosts + template: ingress.yaml + set: + ingress.enabled: true + ingress.hosts: + - host: jellyfin.example.com + paths: + - path: / + pathType: Prefix + asserts: + - hasDocuments: + count: 1 + - isNotNull: + path: spec.rules + + # ============================================================================= + # HTTPROUTE VALIDATIONS + # ============================================================================= + + - it: should not create HTTPRoute by default + template: httproute.yaml + asserts: + - hasDocuments: + count: 0 + + - it: should create HTTPRoute when enabled + template: httproute.yaml + set: + httpRoute.enabled: true + asserts: + - hasDocuments: + count: 1 + + - it: should allow HTTPRoute with empty parentRefs (edge case) + template: httproute.yaml + set: + httpRoute.enabled: true + httpRoute.parentRefs: [] + asserts: + - hasDocuments: + count: 1 + + - it: should allow HTTPRoute with empty hostnames (edge case) + template: httproute.yaml + set: + httpRoute.enabled: true + httpRoute.hostnames: [] + asserts: + - hasDocuments: + count: 1 + + # ============================================================================= + # SERVICE TYPE VALIDATIONS + # ============================================================================= + + - it: should create service with ClusterIP type + template: service.yaml + set: + service.type: ClusterIP + asserts: + - equal: + path: spec.type + value: ClusterIP + + - it: should create service with NodePort type + template: service.yaml + set: + service.type: NodePort + asserts: + - equal: + path: spec.type + value: NodePort + + - it: should create service with LoadBalancer type + template: service.yaml + set: + service.type: LoadBalancer + asserts: + - equal: + path: spec.type + value: LoadBalancer + + # ============================================================================= + # PORT RANGE VALIDATIONS + # ============================================================================= + + # These are validated by values.schema.json, testing templates work correctly + + - it: should accept service.port in valid range (1-65535) + template: service.yaml + set: + service.port: 8096 + asserts: + - equal: + path: spec.ports[0].port + value: 8096 + + - it: should accept nodePort in valid range (30000-32767) + template: service.yaml + set: + service.type: NodePort + service.nodePort: 30096 + asserts: + - equal: + path: spec.ports[0].nodePort + value: 30096 + + - it: should accept nodePort at lower boundary + template: service.yaml + set: + service.type: NodePort + service.nodePort: 30000 + asserts: + - equal: + path: spec.ports[0].nodePort + value: 30000 + + - it: should accept nodePort at upper boundary + template: service.yaml + set: + service.type: NodePort + service.nodePort: 32767 + asserts: + - equal: + path: spec.ports[0].nodePort + value: 32767 + + # ============================================================================= + # DNS POLICY VALIDATIONS + # ============================================================================= + + - it: should not set dnsPolicy by default + template: deployment.yaml + asserts: + - isNull: + path: spec.template.spec.dnsPolicy + + - it: should use ClusterFirstWithHostNet when hostNetwork enabled + template: deployment.yaml + set: + podPrivileges.hostNetwork: true + asserts: + - equal: + path: spec.template.spec.dnsPolicy + value: ClusterFirstWithHostNet + + - it: should allow custom dnsPolicy override + template: deployment.yaml + set: + dnsPolicy: None + asserts: + - equal: + path: spec.template.spec.dnsPolicy + value: None + + # ============================================================================= + # COMPLEX VALIDATION SCENARIOS + # ============================================================================= + + - it: should fail with all conflicting options enabled + template: networkpolicy.yaml + set: + networkPolicy.enabled: true + jellyfin.enableDLNA: true + podPrivileges.hostNetwork: true + asserts: + - failedTemplate: {} + + - it: should succeed with compatible options + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + jellyfin.enableDLNA: false + networkPolicy.enabled: true + persistence.media.type: pvc + service.type: LoadBalancer + asserts: + - template: serviceMonitor.yaml + hasDocuments: + count: 1 + - template: networkpolicy.yaml + hasDocuments: + count: 1 + - template: deployment.yaml + isNull: + path: spec.template.spec.hostNetwork + + - it: should configure for DLNA without NetworkPolicy + set: + jellyfin.enableDLNA: true + networkPolicy.enabled: false + asserts: + - template: deployment.yaml + equal: + path: spec.template.spec.hostNetwork + value: true + - template: networkpolicy.yaml + hasDocuments: + count: 0 + + # ============================================================================= + # REGRESSION TESTS FOR VALIDATIONS + # ============================================================================= + + - it: should maintain hostNetwork=false by default + template: deployment.yaml + asserts: + - isNull: + path: spec.template.spec.hostNetwork + + - it: should maintain NetworkPolicy disabled by default + template: networkpolicy.yaml + asserts: + - hasDocuments: + count: 0 + + - it: should maintain ServiceMonitor disabled by default + template: serviceMonitor.yaml + asserts: + - hasDocuments: + count: 0 + + - it: should allow all features when properly configured + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + networkPolicy.enabled: true + jellyfin.enableDLNA: false + ingress.enabled: true + ingress.hosts: + - host: jellyfin.example.com + paths: + - path: / + pathType: Prefix + asserts: + - template: serviceMonitor.yaml + hasDocuments: + count: 1 + - template: networkpolicy.yaml + hasDocuments: + count: 1 + - template: ingress.yaml + hasDocuments: + count: 1 + - template: deployment.yaml + isNull: + path: spec.template.spec.hostNetwork diff --git a/charts/jellyfin/values.schema.json b/charts/jellyfin/values.schema.json new file mode 100644 index 0000000..53b249c --- /dev/null +++ b/charts/jellyfin/values.schema.json @@ -0,0 +1,814 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "title": "Jellyfin Helm Chart Values", + "description": "JSON Schema for Jellyfin Helm chart values.yaml with comprehensive validation and mutual exclusivity checks", + "type": "object", + "properties": { + "replicaCount": { + "type": "integer", + "minimum": 1, + "default": 1, + "description": "Number of Jellyfin replicas. Should remain at 1 as Jellyfin does not support horizontal scaling." + }, + "revisionHistoryLimit": { + "type": "integer", + "minimum": 0, + "default": 3, + "description": "Number of old ReplicaSets to retain for rollback history. Set to 0 to disable (not recommended)." + }, + "imagePullSecrets": { + "type": "array", + "default": [], + "items": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "description": "Name of the secret containing registry credentials" + } + } + }, + "description": "Image pull secrets for authenticating with private container registries" + }, + "image": { + "type": "object", + "required": ["repository", "pullPolicy"], + "properties": { + "repository": { + "type": "string", + "default": "docker.io/jellyfin/jellyfin", + "pattern": "^[a-z0-9.-]+(:[0-9]+)?(/[a-z0-9._-]+)*$", + "description": "Container image repository for Jellyfin" + }, + "tag": { + "type": "string", + "default": "", + "description": "Image tag. Leave empty to use appVersion from Chart.yaml" + }, + "pullPolicy": { + "type": "string", + "enum": ["Always", "IfNotPresent", "Never"], + "default": "IfNotPresent", + "description": "Image pull policy" + } + } + }, + "nameOverride": { + "type": "string", + "default": "", + "description": "Override the chart name in resource names" + }, + "fullnameOverride": { + "type": "string", + "default": "", + "description": "Override the full resource name (namespace-name)" + }, + "serviceAccount": { + "type": "object", + "properties": { + "create": { + "type": "boolean", + "default": true, + "description": "Create a dedicated service account" + }, + "automount": { + "type": "boolean", + "default": true, + "description": "Automatically mount service account token" + }, + "annotations": { + "type": "object", + "default": {}, + "description": "Annotations to add to the service account" + }, + "name": { + "type": "string", + "default": "", + "description": "Custom name for service account. Auto-generated if empty." + } + } + }, + "podAnnotations": { + "type": "object", + "default": {}, + "description": "Annotations to add to Jellyfin pods" + }, + "podLabels": { + "type": "object", + "default": {}, + "description": "Additional labels for Jellyfin pods" + }, + "podSecurityContext": { + "type": "object", + "default": {}, + "description": "Security context for the pod (fsGroup, runAsUser, etc.)" + }, + "securityContext": { + "type": "object", + "default": {}, + "description": "Security context for the container (capabilities, readOnlyRootFilesystem, etc.)" + }, + "runtimeClassName": { + "type": "string", + "default": "", + "description": "RuntimeClass for the pod (e.g., nvidia, kata)" + }, + "priorityClassName": { + "type": "string", + "default": "", + "description": "PriorityClass name for pod scheduling priority" + }, + "dnsConfig": { + "type": "object", + "default": {}, + "description": "Custom DNS configuration for the pod" + }, + "dnsPolicy": { + "type": "string", + "default": "", + "enum": ["", "ClusterFirst", "ClusterFirstWithHostNet", "Default", "None"], + "description": "DNS policy for the pod" + }, + "deploymentStrategy": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["RollingUpdate", "Recreate"], + "default": "RollingUpdate", + "description": "Deployment update strategy" + } + } + }, + "deploymentAnnotations": { + "type": "object", + "default": {}, + "description": "Annotations for the Deployment resource" + }, + "service": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["ClusterIP", "NodePort", "LoadBalancer", "ExternalName"], + "default": "ClusterIP", + "description": "Kubernetes service type" + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "default": 8096, + "description": "Service port for Jellyfin web interface" + }, + "nodePort": { + "type": "integer", + "minimum": 30000, + "maximum": 32767, + "description": "NodePort when service type is NodePort" + }, + "annotations": { + "type": "object", + "default": {}, + "description": "Service annotations" + }, + "ipFamilyPolicy": { + "type": "string", + "enum": ["", "SingleStack", "PreferDualStack", "RequireDualStack"], + "default": "", + "description": "IP family policy for dual-stack support" + }, + "ipFamilies": { + "type": "array", + "items": { + "type": "string", + "enum": ["IPv4", "IPv6"] + }, + "uniqueItems": true, + "maxItems": 2, + "default": [], + "description": "IP families (IPv4, IPv6, or both)" + } + } + }, + "ingress": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable Ingress resource" + }, + "className": { + "type": "string", + "description": "IngressClass name (e.g., nginx, traefik)" + }, + "annotations": { + "type": "object", + "default": {}, + "description": "Ingress annotations" + }, + "hosts": { + "type": "array", + "items": { + "type": "object", + "required": ["host"], + "properties": { + "host": { + "type": "string", + "format": "hostname", + "description": "Hostname for Ingress" + }, + "paths": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "URL path" + }, + "pathType": { + "type": "string", + "enum": ["Prefix", "Exact", "ImplementationSpecific"], + "description": "Path matching type" + } + } + } + } + } + } + }, + "tls": { + "type": "array", + "items": { + "type": "object" + } + } + } + }, + "httpRoute": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable Gateway API HTTPRoute" + }, + "parentRefs": { + "type": "array", + "items": { + "type": "object" + } + }, + "hostnames": { + "type": "array", + "items": { + "type": "string", + "format": "hostname" + } + }, + "rules": { + "type": "array", + "items": { + "type": "object" + } + } + } + }, + "resources": { + "type": "object", + "default": {}, + "description": "CPU/Memory resource requests and limits" + }, + "startupProbe": { + "type": ["object", "null"], + "default": null, + "description": "Startup probe configuration. Useful for large media libraries with slow startup." + }, + "livenessProbe": { + "type": "object", + "description": "Liveness probe configuration" + }, + "readinessProbe": { + "type": "object", + "description": "Readiness probe configuration" + }, + "volumes": { + "type": "array", + "default": [], + "items": { + "type": "object" + }, + "description": "Additional volumes for the pod" + }, + "volumeMounts": { + "type": "array", + "default": [], + "items": { + "type": "object" + }, + "description": "Additional volume mounts for the container" + }, + "nodeSelector": { + "type": "object", + "default": {}, + "description": "Node labels for pod assignment" + }, + "tolerations": { + "type": "array", + "default": [], + "items": { + "type": "object" + }, + "description": "Tolerations for pod assignment" + }, + "affinity": { + "type": "object", + "default": {}, + "description": "Affinity rules for pod assignment" + }, + "podPrivileges": { + "type": "object", + "properties": { + "hostIPC": { + "type": "boolean", + "default": false, + "description": "Enable hostIPC namespace. Required for NVIDIA MPS GPU sharing." + }, + "hostNetwork": { + "type": "boolean", + "default": false, + "description": "Enable hostNetwork. Required for DLNA. Mutually exclusive with NetworkPolicy." + }, + "hostPID": { + "type": "boolean", + "default": false, + "description": "Enable hostPID namespace" + } + } + }, + "jellyfin": { + "type": "object", + "properties": { + "enableDLNA": { + "type": "boolean", + "default": false, + "description": "Enable DLNA support. Requires hostNetwork=true. Mutually exclusive with NetworkPolicy." + }, + "command": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "Override container entrypoint" + }, + "args": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "Additional arguments for entrypoint" + }, + "envFrom": { + "type": "array", + "default": [], + "items": { + "type": "object", + "properties": { + "configMapRef": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "optional": { + "type": "boolean" + } + } + }, + "secretRef": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "optional": { + "type": "boolean" + } + } + }, + "prefix": { + "type": "string" + } + } + }, + "description": "Load environment variables from ConfigMap or Secret" + }, + "env": { + "type": "array", + "default": [], + "items": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "valueFrom": { + "type": "object" + } + } + }, + "description": "Additional environment variables" + } + } + }, + "persistence": { + "type": "object", + "properties": { + "config": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable persistent storage for config. False uses emptyDir." + }, + "type": { + "type": "string", + "enum": ["pvc", "hostPath", "emptyDir"], + "default": "pvc", + "description": "Storage type: pvc, hostPath, or emptyDir" + }, + "accessMode": { + "type": "string", + "enum": ["ReadWriteOnce", "ReadOnlyMany", "ReadWriteMany", "ReadWriteOncePod"], + "default": "ReadWriteOnce", + "description": "PVC access mode" + }, + "size": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)?(Ki|Mi|Gi|Ti|Pi|Ei|K|M|G|T|P|E)?$", + "default": "5Gi", + "description": "PVC size (e.g., 5Gi, 10Gi)" + }, + "storageClass": { + "type": "string", + "default": "", + "description": "StorageClass name. Empty uses default provisioner." + }, + "existingClaim": { + "type": "string", + "description": "Use existing PVC instead of creating new" + }, + "hostPath": { + "type": "string", + "description": "Host path when type is hostPath" + }, + "annotations": { + "type": "object", + "default": {}, + "description": "PVC annotations" + } + } + }, + "media": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable persistent storage for media" + }, + "type": { + "type": "string", + "enum": ["pvc", "hostPath", "emptyDir"], + "default": "pvc", + "description": "Storage type: pvc, hostPath, or emptyDir" + }, + "accessMode": { + "type": "string", + "enum": ["ReadWriteOnce", "ReadOnlyMany", "ReadWriteMany", "ReadWriteOncePod"], + "default": "ReadWriteOnce", + "description": "PVC access mode" + }, + "size": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)?(Ki|Mi|Gi|Ti|Pi|Ei|K|M|G|T|P|E)?$", + "default": "25Gi", + "description": "PVC size" + }, + "storageClass": { + "type": "string", + "default": "", + "description": "StorageClass name" + }, + "existingClaim": { + "type": "string", + "description": "Use existing PVC" + }, + "hostPath": { + "type": "string", + "description": "Host path when type is hostPath" + }, + "annotations": { + "type": "object", + "default": {}, + "description": "PVC annotations" + } + } + }, + "cache": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable dedicated cache volume for better performance" + }, + "type": { + "type": "string", + "enum": ["pvc", "hostPath", "emptyDir"], + "default": "pvc", + "description": "Storage type" + }, + "accessMode": { + "type": "string", + "enum": ["ReadWriteOnce", "ReadOnlyMany", "ReadWriteMany", "ReadWriteOncePod"], + "default": "ReadWriteOnce", + "description": "PVC access mode" + }, + "size": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)?(Ki|Mi|Gi|Ti|Pi|Ei|K|M|G|T|P|E)?$", + "default": "10Gi", + "description": "PVC size" + }, + "storageClass": { + "type": "string", + "default": "", + "description": "StorageClass name (can use faster storage)" + }, + "existingClaim": { + "type": "string", + "description": "Use existing PVC" + }, + "hostPath": { + "type": "string", + "description": "Host path when type is hostPath" + }, + "annotations": { + "type": "object", + "default": {}, + "description": "PVC annotations" + } + } + } + } + }, + "metrics": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable metrics collection" + }, + "serviceMonitor": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Create Prometheus ServiceMonitor" + }, + "interval": { + "type": "string", + "pattern": "^[0-9]+(s|m|h)$", + "description": "Scrape interval (e.g., 30s, 1m)" + }, + "scrapeTimeout": { + "type": "string", + "pattern": "^[0-9]+(s|m|h)$", + "description": "Scrape timeout" + }, + "labels": { + "type": "object", + "description": "Additional labels for ServiceMonitor" + }, + "annotations": { + "type": "object", + "description": "ServiceMonitor annotations" + } + } + } + } + }, + "initContainers": { + "type": "array", + "default": [], + "items": { + "type": "object" + }, + "description": "DEPRECATED: Use extraInitContainers instead. Will be removed after 2030." + }, + "extraInitContainers": { + "type": "array", + "default": [], + "items": { + "type": "object" + }, + "description": "Additional init containers" + }, + "extraContainers": { + "type": "array", + "default": [], + "items": { + "type": "object" + }, + "description": "Additional sidecar containers" + }, + "networkPolicy": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable NetworkPolicy for network isolation. Mutually exclusive with hostNetwork and DLNA." + }, + "policyTypes": { + "type": "array", + "items": { + "type": "string", + "enum": ["Ingress", "Egress"] + }, + "uniqueItems": true, + "minItems": 1, + "default": ["Ingress", "Egress"], + "description": "Policy types to enforce" + }, + "ingress": { + "type": "object", + "properties": { + "allowExternal": { + "type": "boolean", + "default": true, + "description": "Allow access from any pod/namespace. Set false for production." + }, + "podSelector": { + "type": "object", + "default": {}, + "description": "Pod selector for allowed ingress (when allowExternal=false)" + }, + "namespaceSelector": { + "type": "object", + "default": {}, + "description": "Namespace selector for allowed ingress" + }, + "customRules": { + "type": "array", + "default": [], + "items": { + "type": "object" + }, + "description": "Additional custom ingress rules" + } + } + }, + "egress": { + "type": "object", + "properties": { + "allowDNS": { + "type": "boolean", + "default": true, + "description": "Allow DNS resolution. Highly recommended." + }, + "dnsNamespace": { + "type": "string", + "default": "kube-system", + "description": "Namespace where DNS service runs" + }, + "dnsPodSelector": { + "type": "object", + "default": { + "k8s-app": "kube-dns" + }, + "description": "Pod selector for DNS pods" + }, + "allowAllEgress": { + "type": "boolean", + "default": true, + "description": "Allow all internet access for metadata/subtitles" + }, + "restrictedEgress": { + "type": "object", + "properties": { + "allowMetadata": { + "type": "boolean", + "default": true, + "description": "Allow HTTPS/443 for metadata providers" + }, + "allowInCluster": { + "type": "boolean", + "default": true, + "description": "Allow pod-to-pod communication" + }, + "allowedCIDRs": { + "type": "array", + "default": [], + "items": { + "type": "string", + "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$|^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}/[0-9]{1,3}$", + "description": "CIDR notation (e.g., 10.0.0.0/8)" + }, + "description": "Additional allowed IP ranges" + } + } + }, + "customRules": { + "type": "array", + "default": [], + "items": { + "type": "object" + }, + "description": "Additional custom egress rules" + } + } + }, + "metrics": { + "type": "object", + "properties": { + "namespace": { + "type": "string", + "default": "", + "description": "Prometheus namespace. Empty uses same as Jellyfin." + }, + "podSelector": { + "type": "object", + "default": { + "app.kubernetes.io/name": "prometheus" + }, + "description": "Prometheus pod selector" + } + } + } + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "networkPolicy": { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["enabled"] + } + }, + "required": ["networkPolicy"] + }, + "then": { + "not": { + "anyOf": [ + { + "properties": { + "jellyfin": { + "properties": { + "enableDLNA": { + "const": true + } + }, + "required": ["enableDLNA"] + } + }, + "required": ["jellyfin"] + }, + { + "properties": { + "podPrivileges": { + "properties": { + "hostNetwork": { + "const": true + } + }, + "required": ["hostNetwork"] + } + }, + "required": ["podPrivileges"] + } + ] + } + } + } + ] +} diff --git a/charts/jellyfin/values.yaml b/charts/jellyfin/values.yaml index ac15613..049f24a 100644 --- a/charts/jellyfin/values.yaml +++ b/charts/jellyfin/values.yaml @@ -120,7 +120,7 @@ service: # -- External traffic policy (Cluster or Local). # externalTrafficPolicy: Cluster # -- NodePort for the service (if applicable). - # nodePort: + # nodePort: # -- Ingress configuration. See: https://kubernetes.io/docs/concepts/services-networking/ingress/ ingress: diff --git a/ct.yaml b/ct.yaml new file mode 100644 index 0000000..171f779 --- /dev/null +++ b/ct.yaml @@ -0,0 +1,12 @@ +# chart-testing configuration +# https://github.com/helm/chart-testing + +remote: origin +target-branch: master +chart-dirs: + - charts +chart-repos: [] +helm-extra-args: --timeout 600s +validate-maintainers: false +check-version-increment: true +excluded-charts: []