Merge pull request #98 from lexfrei/chore/testing-and-validation
This commit is contained in:
39 files changed
+8757
-724
No files matched your search
@@ -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'
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
+16
-31
@@ -6,47 +6,31 @@ A Helm chart for Jellyfin Media Server
|
||||
|
||||
**Homepage:** <https://jellyfin.org/>
|
||||
|
||||
## 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
|
||||
```
|
||||
helm repo update
|
||||
|
||||
### 2. Install the Helm Chart
|
||||
|
||||
To install a chart, use the following command:
|
||||
|
||||
```bash
|
||||
# Install with default settings (ephemeral storage)
|
||||
helm install my-jellyfin jellyfin/jellyfin
|
||||
|
||||
# Install with persistent storage
|
||||
helm install my-jellyfin jellyfin/jellyfin \
|
||||
--set persistence.config.enabled=true \
|
||||
--set persistence.media.enabled=true
|
||||
```
|
||||
|
||||
### 3. View the Installation
|
||||
|
||||
You can check the status of the release using:
|
||||
For production deployments, create a custom `values.yaml` file with your configuration and install using:
|
||||
|
||||
```bash
|
||||
helm status my-jellyfin
|
||||
helm install my-jellyfin jellyfin/jellyfin --values values.yaml
|
||||
```
|
||||
|
||||
## Customizing the Chart
|
||||
## Maintainers
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
### 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
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
helm repo update
|
||||
|
||||
### 2. Install the Helm Chart
|
||||
|
||||
To install a chart, use the following command:
|
||||
|
||||
```bash
|
||||
# Install with default settings (ephemeral storage)
|
||||
helm install my-jellyfin jellyfin/jellyfin
|
||||
|
||||
# Install with persistent storage
|
||||
helm install my-jellyfin jellyfin/jellyfin \
|
||||
--set persistence.config.enabled=true \
|
||||
--set persistence.media.enabled=true
|
||||
```
|
||||
|
||||
### 3. View the Installation
|
||||
|
||||
You can check the status of the release using:
|
||||
For production deployments, create a custom `values.yaml` file with your configuration and install using:
|
||||
|
||||
```bash
|
||||
helm status my-jellyfin
|
||||
helm install my-jellyfin jellyfin/jellyfin --values values.yaml
|
||||
```
|
||||
|
||||
## Customizing the Chart
|
||||
## Maintainers
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
{{ 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
|
||||
```
|
||||
|
||||
|
||||
@@ -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/)
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,6 +1,6 @@
|
||||
{{- if .Values.initContainers }}
|
||||
#############################################################################
|
||||
## ⚠️ WARNING ⚠️ ##
|
||||
## *** WARNING *** ##
|
||||
## ##
|
||||
## The 'initContainers' parameter is DEPRECATED! ##
|
||||
## Please migrate to 'extraInitContainers' instead. ##
|
||||
@@ -13,293 +13,75 @@
|
||||
## ##
|
||||
#############################################################################
|
||||
|
||||
{{- 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 ##
|
||||
## ##
|
||||
#############################################################################
|
||||
|
||||
{{- end }}
|
||||
Thank you for installing {{ .Chart.Name }}!
|
||||
|
||||
Your release is named {{ .Release.Name }}.
|
||||
|
||||
To learn more about the release, try:
|
||||
|
||||
$ helm status {{ .Release.Name }} --namespace {{ .Release.Namespace }}
|
||||
$ helm get all {{ .Release.Name }} --namespace {{ .Release.Namespace }}
|
||||
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
{{- range .paths }}
|
||||
|
||||
Jellyfin is available at:
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||
{{- end }}
|
||||
Jellyfin is available via Ingress at:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }}{{ (index .paths 0).path }}
|
||||
{{- end }}
|
||||
{{- else if .Values.httpRoute.enabled }}
|
||||
|
||||
Jellyfin is available via Gateway API HTTPRoute.
|
||||
{{- if .Values.httpRoute.hostnames }}
|
||||
Hostnames configured:
|
||||
{{- range .Values.httpRoute.hostnames }}
|
||||
- {{ . }}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
⚠ No hostnames configured. Set httpRoute.hostnames for external access.
|
||||
{{- 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"
|
||||
|
||||
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" . }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
|
||||
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 }}
|
||||
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 }}
|
||||
|
||||
WARNING: Configuration persistence is disabled!
|
||||
Data will be lost when the pod is restarted.
|
||||
Enable persistence.config.enabled to persist your configuration.
|
||||
{{- end }}
|
||||
To access Jellyfin, use port-forwarding:
|
||||
|
||||
{{- 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 }}
|
||||
{{- end }}
|
||||
Rules: {{ len .Values.httpRoute.rules }} route(s) configured
|
||||
Backend: {{ include "jellyfin.fullname" . }}:{{ .Values.service.port }}
|
||||
|
||||
Access Jellyfin via Gateway API at:
|
||||
{{- if .Values.httpRoute.hostnames }}
|
||||
{{- 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 }}
|
||||
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.
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.networkPolicy.enabled }}
|
||||
{{- if not .Values.persistence.config.enabled }}
|
||||
|
||||
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 }}
|
||||
|
||||
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 }}
|
||||
|
||||
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: NetworkPolicy requires a CNI plugin with NetworkPolicy support (Calico, Cilium, etc.)
|
||||
⚠ WARNING: Configuration persistence is disabled. Data will be lost on pod restart.
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
{{- if not .Values.persistence.media.enabled }}
|
||||
|
||||
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 }}
|
||||
|
||||
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 }}
|
||||
⚠ WARNING: Media persistence is disabled. Media files will be lost on pod restart.
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.startupProbe }}
|
||||
{{- $startupTimeout := mul (default 10 .Values.startupProbe.periodSeconds) (default 30 .Values.startupProbe.failureThreshold) }}
|
||||
|
||||
Startup Configuration:
|
||||
Startup Probe: Enabled
|
||||
Max Startup Time: {{ $startupTimeout }}s ({{ div $startupTimeout 60 }} minutes)
|
||||
|
||||
The startup probe gives Jellyfin enough time to initialize, especially
|
||||
with large media libraries. After startup succeeds, liveness and
|
||||
readiness probes take over for health monitoring.
|
||||
|
||||
{{- if gt $startupTimeout 600 }}
|
||||
⚠ NOTE: Startup timeout is very long ({{ div $startupTimeout 60 }}+ minutes).
|
||||
Consider optimizing your media library or reducing failureThreshold.
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
Persistence Configuration:
|
||||
Config: {{ if .Values.persistence.config.enabled }}{{ .Values.persistence.config.type | default "pvc" }}{{ if .Values.persistence.config.existingClaim }} (existing: {{ .Values.persistence.config.existingClaim }}){{ end }}{{ else }}emptyDir{{ end }}
|
||||
Media: {{ if .Values.persistence.media.enabled }}{{ .Values.persistence.media.type | default "pvc" }}{{ if .Values.persistence.media.existingClaim }} (existing: {{ .Values.persistence.media.existingClaim }}){{ end }}{{ else }}emptyDir{{ end }}
|
||||
Cache: {{ if .Values.persistence.cache.enabled }}{{ .Values.persistence.cache.type | default "pvc" }}{{ if .Values.persistence.cache.existingClaim }} (existing: {{ .Values.persistence.cache.existingClaim }}){{ end }}{{ else }}emptyDir{{ end }}
|
||||
|
||||
{{- if and .Values.persistence.cache.enabled (eq .Values.persistence.cache.type "pvc") }}
|
||||
Cache Volume:
|
||||
Dedicated cache volume is enabled for improved performance.
|
||||
Size: {{ .Values.persistence.cache.size }}
|
||||
Access Mode: {{ .Values.persistence.cache.accessMode }}
|
||||
{{- if .Values.persistence.cache.storageClass }}
|
||||
Storage Class: {{ .Values.persistence.cache.storageClass }}
|
||||
{{- end }}
|
||||
|
||||
Benefits:
|
||||
- Faster transcoding and metadata operations
|
||||
- Separate cache lifecycle from config/media
|
||||
- Can use faster storage class (SSD) for cache
|
||||
{{- end }}
|
||||
|
||||
{{- if and .Values.persistence.cache.enabled (eq .Values.persistence.cache.type "hostPath") }}
|
||||
⚠ WARNING: Using hostPath for cache - ensure {{ .Values.persistence.cache.hostPath }} exists on the node
|
||||
{{- 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 }}
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user